@fiale-plus/pi-rogue-bundle 0.1.19 → 0.1.20
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 +5 -5
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +2 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +20 -11
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +1 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +98 -11
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +205 -33
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +12 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +32 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +24 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +37 -0
- package/package.json +2 -2
- package/src/extension.test.ts +6 -6
- package/src/extension.ts +5 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ It stitches together (and bundles for a true single-package install):
|
|
|
6
6
|
|
|
7
7
|
- `@fiale-plus/pi-core` (shared contracts/helpers)
|
|
8
8
|
- `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
|
|
9
|
-
- `@fiale-plus/pi-rogue-context-broker` (
|
|
9
|
+
- `@fiale-plus/pi-rogue-context-broker` (context-broker runtime; registered by default with an env kill switch)
|
|
10
10
|
- `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
|
|
11
11
|
|
|
12
12
|
Direct installs of the advisor/orchestration packages are paused (marked private). All users and future releases go through the bundle. See `docs/release.md` and root `AGENTS.md` / `README.md` for the release policy.
|
|
@@ -28,9 +28,9 @@ npm install
|
|
|
28
28
|
## Scope boundaries
|
|
29
29
|
|
|
30
30
|
- **Lab / internal helpers are excluded from this bundle.**
|
|
31
|
-
- The
|
|
32
|
-
-
|
|
33
|
-
- Set `PI_CONTEXT_BROKER_ENABLED=
|
|
31
|
+
- The context-broker runtime is bundled and registered by default in the bundle.
|
|
32
|
+
- Consumers can import the runtime through the bundle subpath: `@fiale-plus/pi-rogue-bundle/context-broker`.
|
|
33
|
+
- Set `PI_CONTEXT_BROKER_ENABLED=false` before starting Pi to disable the `/context` command surface and prompt-load rewriting.
|
|
34
34
|
- Optional durable broker storage can be enabled with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`; it defaults to SQLite/FTS and supports `PI_CONTEXT_BROKER_BACKEND=jsonl` for the legacy JSONL/blob backend.
|
|
35
35
|
- `@fiale-plus/pi-rogue-bundle` is the only published surface for the logic.
|
|
36
36
|
- Internal helper packages (`@fiale-plus/pi-rogue-guardrails`, `@fiale-plus/pi-rogue-brain`, `@fiale-plus/pi-rogue-repo-arch`) are maintained separately in the lab section and not published.
|
|
@@ -38,7 +38,7 @@ npm install
|
|
|
38
38
|
## Command surface
|
|
39
39
|
|
|
40
40
|
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
|
|
41
|
-
-
|
|
41
|
+
- Context broker: enabled by default; `PI_CONTEXT_BROKER_ENABLED=false` disables `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, `/context export <handle>`, and `/context prune` with autocomplete.
|
|
42
42
|
|
|
43
43
|
## Status
|
|
44
44
|
|
|
@@ -8,37 +8,46 @@ This package contains the executable in-memory bounded broker implementation:
|
|
|
8
8
|
- Lookups support handle, session, kind, tag, path, command prefix, branch, tier, and text filters.
|
|
9
9
|
- Omitted summaries become metadata-only placeholders, keeping raw payloads out of prompt briefs by default.
|
|
10
10
|
- Artifacts are classified as hot/warm/cold on publish; prompt briefs render hot first, warm second, and exclude cold unless explicitly queried.
|
|
11
|
-
- Pruning enforces per-session record/byte caps,
|
|
11
|
+
- Pruning enforces per-session record/byte caps, optional global (cross-session) record/byte caps, tier-specific caps, TTL expiry on reads, and pinned-artifact retention.
|
|
12
12
|
|
|
13
|
-
It is
|
|
13
|
+
It is registered by default in the bundle, with an explicit env kill switch.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Mainline extension
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The bundle registers a `context_lookup` LLM tool plus `/context` commands by default. To disable the runtime for rollback:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
PI_CONTEXT_BROKER_ENABLED=
|
|
20
|
+
PI_CONTEXT_BROKER_ENABLED=false pi
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
When
|
|
23
|
+
When active, the bundle registers:
|
|
24
24
|
|
|
25
|
-
- `/context status` — enabled state, record/byte counts, pinned counts.
|
|
25
|
+
- `/context status` — enabled state, record/byte counts, pinned counts, and routing telemetry.
|
|
26
26
|
- `/context brief` — bounded prompt-safe broker brief with handles and summaries.
|
|
27
27
|
- `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
|
|
28
28
|
- `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
|
|
29
|
+
- `/context export <handle>` — write full payload to a temp file without dumping it into prompt.
|
|
29
30
|
- `/context prune` — run TTL/cap pruning immediately.
|
|
30
31
|
|
|
31
32
|
The command includes autocomplete for subcommands and known artifact handles. Exact handle lookup returns clipped payload text; text search returns a smaller clipped excerpt, and truncation is marked explicitly.
|
|
32
33
|
|
|
33
34
|
Optional durability is available with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`. Durable mode now defaults to SQLite (`artifacts.sqlite`) with an FTS index for text lookup, so exact handles, tier, and pin state survive restarts without replay reconstruction. Set `PI_CONTEXT_BROKER_BACKEND=jsonl` to use the legacy JSONL/blob backend.
|
|
34
35
|
|
|
36
|
+
- `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` controls when large `toolResult` / `bashExecution` payloads are rewritten in-context. The default is `0` (rewritten), so raw tool evidence is replaced by handles by default.
|
|
37
|
+
|
|
38
|
+
For quieter sessions, set `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` to a higher value to only rewrite larger outputs.
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
## Session behavior and limits
|
|
36
42
|
|
|
37
|
-
- On session start/reload, the
|
|
43
|
+
- On session start/reload, the runtime backfills the current Pi session branch from `toolResult` and prompt-visible `bashExecution` entries.
|
|
38
44
|
- Backfill is idempotent by session entry id, skips malformed entries instead of failing the session, and honors Pi's `excludeFromContext` bash entries.
|
|
39
45
|
- Without durable mode, restarting Pi loses broker state until the current branch is backfilled again.
|
|
40
|
-
- Prompt integration injects a bounded, tier-aware broker brief and lookup guidance; the LLM also gets a `context_lookup` tool for exact handle dereferencing.
|
|
41
|
-
- The `context` hook rewrites
|
|
46
|
+
- Prompt integration injects a bounded, tier-aware broker brief and lookup guidance; the LLM also gets a `context_lookup` tool for exact handle dereferencing. Payloads that hit hostile-binary heuristics are represented in prompt as handles plus short guidance to export the full content.
|
|
47
|
+
- The `context` hook rewrites prompt-visible `toolResult` and `bashExecution` payloads in the LLM-bound message copy to broker handles and summaries, reducing prompt load while preserving exact `/context lookup` rehydration.
|
|
42
48
|
- Pi `excludeFromContext` bash entries are not backfilled or rewritten into broker prompts.
|
|
43
49
|
- Basic secret redaction runs before broker storage and display for common token/password/API-key patterns.
|
|
44
|
-
-
|
|
50
|
+
- Optional global caps can be configured via env vars:
|
|
51
|
+
- `PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS`
|
|
52
|
+
- `PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES`
|
|
53
|
+
- Rollback is immediate: set `PI_CONTEXT_BROKER_ENABLED=false` and `/reload` or restart Pi. Disable durable writes by unsetting `PI_CONTEXT_BROKER_DURABLE` and `PI_CONTEXT_BROKER_STORE_DIR`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-context-broker",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Context broker runtime for Pi-Rogue with bounded handle-first prompt payload storage.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@ async function runHandlers(handlers: Map<string, any[]>, name: string, event: an
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
describe("context broker
|
|
55
|
+
describe("context broker extension enablement", () => {
|
|
56
56
|
const oldEnv = process.env.PI_CONTEXT_BROKER_ENABLED;
|
|
57
57
|
|
|
58
58
|
afterEach(() => {
|
|
@@ -117,7 +117,7 @@ describe("context broker beta enablement", () => {
|
|
|
117
117
|
|
|
118
118
|
expect(notifications[0].message).toContain("Backfilled 2/2");
|
|
119
119
|
expect(notifications[1].message).toContain("Backfilled 0/2");
|
|
120
|
-
expect(notifications.
|
|
120
|
+
expect(notifications.find((entry) => entry.message.includes("Context broker: enabled"))?.message).toContain("records=2");
|
|
121
121
|
expect(notifications.at(-1)?.message).toContain("README.md");
|
|
122
122
|
});
|
|
123
123
|
|
|
@@ -251,6 +251,33 @@ describe("context broker beta enablement", () => {
|
|
|
251
251
|
rmSync(exportPath);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
it("omits hostile payloads from lookup output and suggests export", async () => {
|
|
255
|
+
const { pi, handlers, commands } = createPiMock();
|
|
256
|
+
registerContextBrokerBeta(pi);
|
|
257
|
+
const { ctx, notifications } = createCtx();
|
|
258
|
+
const payload = `safe\u0000binary${"\u0007".repeat(12)}tail`;
|
|
259
|
+
|
|
260
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
261
|
+
await runHandlers(handlers, "tool_result", {
|
|
262
|
+
type: "tool_result",
|
|
263
|
+
toolCallId: "call-hostile",
|
|
264
|
+
toolName: "bash",
|
|
265
|
+
input: { command: "printf host" },
|
|
266
|
+
content: [{ type: "text", text: payload }],
|
|
267
|
+
isError: false,
|
|
268
|
+
}, ctx);
|
|
269
|
+
|
|
270
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
271
|
+
expect(lookupCompletion?.value.startsWith("lookup ctx://")).toBe(true);
|
|
272
|
+
const lookupHandle = lookupCompletion?.value.replace(/^lookup /, "");
|
|
273
|
+
|
|
274
|
+
await commands.get("context").handler(`lookup ${lookupHandle}`, ctx);
|
|
275
|
+
const commandMessage = notifications.at(-1)?.message ?? "";
|
|
276
|
+
expect(commandMessage).toContain("payload intentionally omitted from prompt");
|
|
277
|
+
expect(commandMessage).toContain("/context export");
|
|
278
|
+
expect(commandMessage).not.toContain("\u0000");
|
|
279
|
+
});
|
|
280
|
+
|
|
254
281
|
it("text search lookup returns a smaller byte-clipped excerpt", async () => {
|
|
255
282
|
const { pi, handlers, commands } = createPiMock();
|
|
256
283
|
registerContextBrokerBeta(pi, { lookupBytes: 80, searchBytes: 50 });
|
|
@@ -277,7 +304,7 @@ describe("context broker beta enablement", () => {
|
|
|
277
304
|
const { pi, handlers, commands } = createPiMock();
|
|
278
305
|
registerContextBrokerBeta(pi);
|
|
279
306
|
const { ctx, notifications } = createCtx();
|
|
280
|
-
const rawPayload = `${"SAFE"}\u0000${"
|
|
307
|
+
const rawPayload = `${"SAFE"}\u0000${"x".repeat(220)}`;
|
|
281
308
|
|
|
282
309
|
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
283
310
|
await runHandlers(handlers, "tool_result", {
|
|
@@ -294,7 +321,6 @@ describe("context broker beta enablement", () => {
|
|
|
294
321
|
|
|
295
322
|
const message = notifications.at(-1)?.message ?? "";
|
|
296
323
|
expect(message).toContain("\\u0000");
|
|
297
|
-
expect(message).toContain("\\u001b");
|
|
298
324
|
expect(message).not.toContain(String.fromCharCode(0));
|
|
299
325
|
});
|
|
300
326
|
|
|
@@ -319,6 +345,44 @@ describe("context broker beta enablement", () => {
|
|
|
319
345
|
expect(result.content[0].text).toContain("exact evidence payload");
|
|
320
346
|
});
|
|
321
347
|
|
|
348
|
+
it("reports routing telemetry in /context status", async () => {
|
|
349
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
350
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1, lookupBytes: 500, searchBytes: 500 });
|
|
351
|
+
const { ctx, notifications } = createCtx();
|
|
352
|
+
|
|
353
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
354
|
+
await runHandlers(handlers, "tool_result", {
|
|
355
|
+
type: "tool_result",
|
|
356
|
+
toolCallId: "call-tool-telemetry",
|
|
357
|
+
toolName: "bash",
|
|
358
|
+
input: { command: "echo telemetry" },
|
|
359
|
+
content: [{ type: "text", text: "telemetry_payload_" + "x".repeat(200) }],
|
|
360
|
+
isError: false,
|
|
361
|
+
}, ctx);
|
|
362
|
+
|
|
363
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
364
|
+
const toolResult = await tools.get("context_lookup").execute("lookup-call", { handle }, undefined, undefined, ctx);
|
|
365
|
+
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
366
|
+
await commands.get("context").handler(`pin ${handle}`, ctx);
|
|
367
|
+
await commands.get("context").handler(`export ${handle}`, ctx);
|
|
368
|
+
const result = await handlers.get("context")?.[0]({
|
|
369
|
+
type: "context",
|
|
370
|
+
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(150) }], isError: false, timestamp: 1 }],
|
|
371
|
+
}, ctx);
|
|
372
|
+
|
|
373
|
+
await commands.get("context").handler("status", ctx);
|
|
374
|
+
|
|
375
|
+
expect(handle).toBeTruthy();
|
|
376
|
+
expect(toolResult.content[0].text).toContain("telemetry_payload_");
|
|
377
|
+
expect(result).toBeDefined();
|
|
378
|
+
const telemetry = notifications.at(-1)?.message ?? "";
|
|
379
|
+
expect(telemetry).toContain("Context broker routing telemetry:");
|
|
380
|
+
expect(telemetry).toContain("lookups tool(calls=");
|
|
381
|
+
expect(telemetry).toContain("lookups slash(calls=");
|
|
382
|
+
expect(telemetry).toContain("exports=");
|
|
383
|
+
expect(telemetry).toContain("pins=");
|
|
384
|
+
});
|
|
385
|
+
|
|
322
386
|
it("does not broker context_lookup results recursively", async () => {
|
|
323
387
|
const { pi, handlers, commands, tools } = createPiMock();
|
|
324
388
|
registerContextBrokerBeta(pi, { lookupBytes: 500, rewriteThresholdBytes: 1 });
|
|
@@ -429,9 +493,9 @@ describe("context broker beta enablement", () => {
|
|
|
429
493
|
expect(notifications.at(-1)?.message).toContain("RAW_TOOL_OUTPUT_");
|
|
430
494
|
});
|
|
431
495
|
|
|
432
|
-
it("
|
|
496
|
+
it("rewrites small tool results and leaves excluded bash outputs unchanged in context", async () => {
|
|
433
497
|
const { pi, handlers } = createPiMock();
|
|
434
|
-
registerContextBrokerBeta(pi
|
|
498
|
+
registerContextBrokerBeta(pi);
|
|
435
499
|
const { ctx } = createCtx();
|
|
436
500
|
const secret = "SECRET_TOKEN=" + "z".repeat(80);
|
|
437
501
|
|
|
@@ -444,7 +508,8 @@ describe("context broker beta enablement", () => {
|
|
|
444
508
|
],
|
|
445
509
|
}, ctx);
|
|
446
510
|
|
|
447
|
-
expect(result).
|
|
511
|
+
expect(result?.messages[0].content?.[0]?.text).toContain("Context broker artifact");
|
|
512
|
+
expect(result?.messages[1]).toMatchObject({ role: "bashExecution", output: secret });
|
|
448
513
|
});
|
|
449
514
|
|
|
450
515
|
it("does not collapse repeated bash rewrites for the same command and timestamp", async () => {
|
|
@@ -497,7 +562,8 @@ describe("context broker beta enablement", () => {
|
|
|
497
562
|
.map((message: any) => String(message.content?.[0]?.text ?? "").match(/ctx:\/\/\S+/)?.[0])
|
|
498
563
|
.filter(Boolean);
|
|
499
564
|
expect(handles.length).toBeLessThanOrEqual(2);
|
|
500
|
-
expect(result.messages.
|
|
565
|
+
expect(result.messages[0].content[0].text).toContain("Context broker artifact pruned before prompt assembly");
|
|
566
|
+
expect(result.messages[0].content[0].text).not.toContain("RAW_0_");
|
|
501
567
|
|
|
502
568
|
for (const handle of handles) {
|
|
503
569
|
await commands.get("context").handler(`lookup ${handle}`, ctx);
|
|
@@ -506,6 +572,27 @@ describe("context broker beta enablement", () => {
|
|
|
506
572
|
}
|
|
507
573
|
});
|
|
508
574
|
|
|
575
|
+
it("does not restore pruned hostile payloads into prompt context", async () => {
|
|
576
|
+
const { pi, handlers } = createPiMock();
|
|
577
|
+
registerContextBrokerBeta(pi, { maxRecords: 1 });
|
|
578
|
+
const { ctx } = createCtx();
|
|
579
|
+
const hostile = `HOSTILE_RAW\u0000${"\u0007".repeat(20)}`;
|
|
580
|
+
|
|
581
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
582
|
+
const result = await handlers.get("context")?.[0]({
|
|
583
|
+
type: "context",
|
|
584
|
+
messages: [
|
|
585
|
+
{ role: "toolResult", toolCallId: "hostile-one", toolName: "bash", content: [{ type: "text", text: hostile }], isError: false, timestamp: 1 },
|
|
586
|
+
{ role: "toolResult", toolCallId: "hostile-two", toolName: "bash", content: [{ type: "text", text: `SECOND\u0000${"\u0007".repeat(20)}` }], isError: false, timestamp: 2 },
|
|
587
|
+
],
|
|
588
|
+
}, ctx);
|
|
589
|
+
|
|
590
|
+
const firstText = result.messages[0].content[0].text;
|
|
591
|
+
expect(firstText).toContain("Raw hostile/binary payload omitted from prompt");
|
|
592
|
+
expect(firstText).not.toContain("HOSTILE_RAW");
|
|
593
|
+
expect(firstText).not.toContain("\u0000");
|
|
594
|
+
});
|
|
595
|
+
|
|
509
596
|
it("redacts secrets before storing and displaying payloads", async () => {
|
|
510
597
|
const { pi, handlers, commands } = createPiMock();
|
|
511
598
|
registerContextBrokerBeta(pi);
|
|
@@ -572,7 +659,7 @@ describe("context broker beta enablement", () => {
|
|
|
572
659
|
const dir = mkdtempSync(join(tmpdir(), "ctx-broker-test-"));
|
|
573
660
|
try {
|
|
574
661
|
const first = createPiMock();
|
|
575
|
-
registerContextBrokerBeta(first.pi, { durable: true, storeDir: dir });
|
|
662
|
+
await registerContextBrokerBeta(first.pi, { durable: true, storeDir: dir });
|
|
576
663
|
const { ctx } = createCtx();
|
|
577
664
|
await runHandlers(first.handlers, "session_start", { type: "session_start" }, ctx);
|
|
578
665
|
await runHandlers(first.handlers, "tool_result", {
|
|
@@ -589,7 +676,7 @@ describe("context broker beta enablement", () => {
|
|
|
589
676
|
|
|
590
677
|
const second = createPiMock();
|
|
591
678
|
const secondRun = createCtx();
|
|
592
|
-
registerContextBrokerBeta(second.pi, { durable: true, storeDir: dir });
|
|
679
|
+
await registerContextBrokerBeta(second.pi, { durable: true, storeDir: dir });
|
|
593
680
|
await runHandlers(second.handlers, "session_start", { type: "session_start" }, secondRun.ctx);
|
|
594
681
|
const secondHandle = second.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
595
682
|
await second.commands.get("context").handler(`lookup ${handle}`, secondRun.ctx);
|
|
@@ -597,7 +684,7 @@ describe("context broker beta enablement", () => {
|
|
|
597
684
|
|
|
598
685
|
const third = createPiMock();
|
|
599
686
|
const thirdRun = createCtx();
|
|
600
|
-
registerContextBrokerBeta(third.pi, { durable: true, storeDir: dir });
|
|
687
|
+
await registerContextBrokerBeta(third.pi, { durable: true, storeDir: dir });
|
|
601
688
|
await runHandlers(third.handlers, "session_start", { type: "session_start" }, thirdRun.ctx);
|
|
602
689
|
await third.commands.get("context").handler(`lookup ${secondHandle}`, thirdRun.ctx);
|
|
603
690
|
await third.commands.get("context").handler("brief", thirdRun.ctx);
|
|
@@ -9,12 +9,13 @@ import type { ExtensionAPI, ExtensionContext, ToolResultEvent } from "@earendil-
|
|
|
9
9
|
import type { ContextArtifact } from "@fiale-plus/pi-core";
|
|
10
10
|
import { createFileContextBroker } from "./file.js";
|
|
11
11
|
import { createInMemoryContextBroker } from "./index.js";
|
|
12
|
-
import { createSqliteContextBroker } from "./sqlite.js";
|
|
13
12
|
|
|
14
13
|
export interface ContextBrokerBetaOptions {
|
|
15
14
|
enabled?: boolean;
|
|
16
15
|
maxRecords?: number;
|
|
17
16
|
maxBytes?: number;
|
|
17
|
+
globalMaxRecords?: number;
|
|
18
|
+
globalMaxBytes?: number;
|
|
18
19
|
briefBytes?: number;
|
|
19
20
|
lookupBytes?: number;
|
|
20
21
|
searchBytes?: number;
|
|
@@ -29,7 +30,7 @@ type SessionContextLike = Pick<ExtensionContext, "cwd" | "sessionManager"> & { u
|
|
|
29
30
|
const DEFAULT_BRIEF_BYTES = 1_800;
|
|
30
31
|
const DEFAULT_LOOKUP_BYTES = 12_000;
|
|
31
32
|
const DEFAULT_SEARCH_BYTES = 2_000;
|
|
32
|
-
const DEFAULT_REWRITE_THRESHOLD_BYTES =
|
|
33
|
+
const DEFAULT_REWRITE_THRESHOLD_BYTES = 0;
|
|
33
34
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
34
35
|
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
35
36
|
|
|
@@ -37,6 +38,14 @@ function envFlag(name: string): boolean {
|
|
|
37
38
|
return ENABLED_VALUES.has(String(process.env[name] ?? "").trim().toLowerCase());
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function envNonNegativeInt(name: string): number | undefined {
|
|
42
|
+
const raw = process.env[name];
|
|
43
|
+
if (!raw) return undefined;
|
|
44
|
+
const value = Number.parseInt(raw, 10);
|
|
45
|
+
if (!Number.isFinite(value) || value < 0) return undefined;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function isEnvEnabled(): boolean {
|
|
41
50
|
return envFlag("PI_CONTEXT_BROKER_ENABLED");
|
|
42
51
|
}
|
|
@@ -76,13 +85,57 @@ function sanitizeForPrompt(text: string): string {
|
|
|
76
85
|
return String(text).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function isHostilePayload(payload: string): boolean {
|
|
89
|
+
return hasHostileText(payload);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasHostileText(text: string): boolean {
|
|
93
|
+
let suspicious = 0;
|
|
94
|
+
let scanned = 0;
|
|
95
|
+
for (const char of text.slice(0, 4096)) {
|
|
96
|
+
const code = char.codePointAt(0) ?? 0;
|
|
97
|
+
scanned += 1;
|
|
98
|
+
if (
|
|
99
|
+
code === 0x00
|
|
100
|
+
|| (code >= 0x01 && code <= 0x08)
|
|
101
|
+
|| (code >= 0x0E && code <= 0x1F)
|
|
102
|
+
|| (code >= 0x7F && code <= 0x9F)
|
|
103
|
+
) {
|
|
104
|
+
suspicious += 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (scanned < 12) return suspicious > 0;
|
|
108
|
+
return suspicious / scanned >= 0.05;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasHostileValue(value: unknown): boolean {
|
|
112
|
+
if (typeof value === "string") return hasHostileText(value);
|
|
113
|
+
if (Array.isArray(value)) return value.some(hasHostileValue);
|
|
114
|
+
if (value && typeof value === "object") {
|
|
115
|
+
return Object.values(value as Record<string, unknown>).some((entry) => hasHostileValue(entry));
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
function renderLookupOutput(item: ContextArtifact, payloadLimit: number): string {
|
|
121
|
+
const isBinary = item.tags.includes("hostile") || item.tags.includes("binary");
|
|
122
|
+
const payloadLines = isBinary
|
|
123
|
+
? [
|
|
124
|
+
"payload:",
|
|
125
|
+
"[payload intentionally omitted from prompt for safety; use /context export",
|
|
126
|
+
sanitizeForPrompt(item.handle),
|
|
127
|
+
"for full content]",
|
|
128
|
+
]
|
|
129
|
+
: [
|
|
130
|
+
"payload:",
|
|
131
|
+
truncateUtf8(sanitizeForPrompt(item.payload), payloadLimit),
|
|
132
|
+
];
|
|
133
|
+
|
|
80
134
|
return [
|
|
81
135
|
sanitizeForPrompt(item.handle),
|
|
82
136
|
`tier=${item.tier} kind=${item.kind} bytes=${item.bytes}`,
|
|
83
137
|
`summary=${sanitizeForPrompt(item.summary)}`,
|
|
84
|
-
|
|
85
|
-
truncateUtf8(sanitizeForPrompt(item.payload), payloadLimit),
|
|
138
|
+
...payloadLines,
|
|
86
139
|
].join("\n");
|
|
87
140
|
}
|
|
88
141
|
|
|
@@ -180,11 +233,20 @@ function contextLookupHistoryPlaceholder(): string {
|
|
|
180
233
|
].join("\n");
|
|
181
234
|
}
|
|
182
235
|
|
|
183
|
-
function
|
|
236
|
+
function prunedPayloadPlaceholder(hostile = false): string {
|
|
237
|
+
return [
|
|
238
|
+
"Context broker artifact pruned before prompt assembly.",
|
|
239
|
+
hostile ? "Raw hostile/binary payload omitted from prompt for safety." : "Raw payload omitted from prompt to avoid restoring pruned broker evidence.",
|
|
240
|
+
"Re-run the originating command or use a retained ctx:// handle if exact evidence is still needed.",
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeTool(event: { toolName: string; input?: any; isError?: boolean }, bytes: number, hostile = false): string {
|
|
184
245
|
const command = event.toolName === "bash" ? event.input?.command : undefined;
|
|
185
246
|
const path = event.input?.path;
|
|
186
247
|
const target = command ? ` command=${compact(String(command), 120)}` : path ? ` path=${path}` : "";
|
|
187
|
-
|
|
248
|
+
const marker = hostile ? "; payload marked hostile; use /context export for full content" : "";
|
|
249
|
+
return `${event.isError ? "failed" : "completed"} ${event.toolName}${target}; payload=${bytes} bytes${marker}`;
|
|
188
250
|
}
|
|
189
251
|
|
|
190
252
|
const NON_BROKERED_TOOL_NAMES = new Set(["context_lookup"]);
|
|
@@ -198,7 +260,7 @@ function ttlFromNowFor(createdAt: number | undefined): number | undefined {
|
|
|
198
260
|
return Math.max(DEFAULT_TTL_MS, Date.now() - createdAt + DEFAULT_TTL_MS);
|
|
199
261
|
}
|
|
200
262
|
|
|
201
|
-
export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrokerBetaOptions = {}): void {
|
|
263
|
+
export async function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrokerBetaOptions = {}): Promise<void> {
|
|
202
264
|
const p = pi as any;
|
|
203
265
|
if (p.__piRogueContextBrokerBetaRegistered) return;
|
|
204
266
|
p.__piRogueContextBrokerBetaRegistered = true;
|
|
@@ -206,10 +268,15 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
206
268
|
const briefBytes = options.briefBytes ?? DEFAULT_BRIEF_BYTES;
|
|
207
269
|
const lookupBytes = options.lookupBytes ?? DEFAULT_LOOKUP_BYTES;
|
|
208
270
|
const searchBytes = options.searchBytes ?? DEFAULT_SEARCH_BYTES;
|
|
209
|
-
const rewriteThresholdBytes =
|
|
271
|
+
const rewriteThresholdBytes =
|
|
272
|
+
options.rewriteThresholdBytes
|
|
273
|
+
?? envNonNegativeInt("PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES")
|
|
274
|
+
?? DEFAULT_REWRITE_THRESHOLD_BYTES;
|
|
210
275
|
const brokerOptions = {
|
|
211
276
|
maxRecords: options.maxRecords ?? 64,
|
|
212
277
|
maxBytes: options.maxBytes ?? 8 * 1024 * 1024,
|
|
278
|
+
globalMaxRecords: options.globalMaxRecords ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS"),
|
|
279
|
+
globalMaxBytes: options.globalMaxBytes ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES"),
|
|
213
280
|
briefBytes,
|
|
214
281
|
};
|
|
215
282
|
const durable = options.durable ?? (envFlag("PI_CONTEXT_BROKER_DURABLE") || Boolean(options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR));
|
|
@@ -217,22 +284,55 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
217
284
|
const broker = durable
|
|
218
285
|
? durableBackend === "jsonl"
|
|
219
286
|
? createFileContextBroker({ ...brokerOptions, dir: options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR })
|
|
220
|
-
: createSqliteContextBroker({ ...brokerOptions, dir: options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR })
|
|
287
|
+
: (await import("./sqlite.js")).createSqliteContextBroker({ ...brokerOptions, dir: options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR })
|
|
221
288
|
: createInMemoryContextBroker(brokerOptions);
|
|
222
289
|
const seenSourceIds = new Set<string>();
|
|
223
290
|
const sourceHandles = new Map<string, string>();
|
|
224
291
|
let activeSessionId = process.cwd();
|
|
292
|
+
const routingTelemetry = {
|
|
293
|
+
contextHookCalls: 0,
|
|
294
|
+
contextHookToolResults: 0,
|
|
295
|
+
contextHookToolResultRewrites: 0,
|
|
296
|
+
contextHookToolResultHostile: 0,
|
|
297
|
+
contextHookBash: 0,
|
|
298
|
+
contextHookBashRewrites: 0,
|
|
299
|
+
contextHookBashHostile: 0,
|
|
300
|
+
toolResultEvents: 0,
|
|
301
|
+
toolResultArtifacts: 0,
|
|
302
|
+
backfillScans: 0,
|
|
303
|
+
backfillAdded: 0,
|
|
304
|
+
backfillErrors: 0,
|
|
305
|
+
toolLookupCalls: 0,
|
|
306
|
+
toolLookupExactCalls: 0,
|
|
307
|
+
toolLookupTextCalls: 0,
|
|
308
|
+
toolLookupHits: 0,
|
|
309
|
+
toolLookupMisses: 0,
|
|
310
|
+
commandLookupCalls: 0,
|
|
311
|
+
commandLookupExactCalls: 0,
|
|
312
|
+
commandLookupTextCalls: 0,
|
|
313
|
+
commandLookupHits: 0,
|
|
314
|
+
commandLookupMisses: 0,
|
|
315
|
+
exportCalls: 0,
|
|
316
|
+
pinCalls: 0,
|
|
317
|
+
statusCalls: 0,
|
|
318
|
+
pruneCalls: 0,
|
|
319
|
+
};
|
|
225
320
|
|
|
226
|
-
function
|
|
227
|
-
|
|
321
|
+
function formatRoutingTelemetry(): string {
|
|
322
|
+
const line = [
|
|
323
|
+
`contextHook calls=${routingTelemetry.contextHookCalls}`,
|
|
324
|
+
`toolResults seen=${routingTelemetry.contextHookToolResults} rewritten=${routingTelemetry.contextHookToolResultRewrites} hostile=${routingTelemetry.contextHookToolResultHostile}`,
|
|
325
|
+
`bash seen=${routingTelemetry.contextHookBash} rewritten=${routingTelemetry.contextHookBashRewrites} hostile=${routingTelemetry.contextHookBashHostile}`,
|
|
326
|
+
`lookups tool(calls=${routingTelemetry.toolLookupCalls}, hits=${routingTelemetry.toolLookupHits}, misses=${routingTelemetry.toolLookupMisses})`,
|
|
327
|
+
`lookups slash(calls=${routingTelemetry.commandLookupCalls}, hits=${routingTelemetry.commandLookupHits}, misses=${routingTelemetry.commandLookupMisses})`,
|
|
328
|
+
`exports=${routingTelemetry.exportCalls}`,
|
|
329
|
+
`pins=${routingTelemetry.pinCalls}`,
|
|
330
|
+
`pruneCalls=${routingTelemetry.pruneCalls}`,
|
|
331
|
+
`backfill scans=${routingTelemetry.backfillScans} added=${routingTelemetry.backfillAdded} errors=${routingTelemetry.backfillErrors}`,
|
|
332
|
+
];
|
|
333
|
+
return `Context broker routing telemetry: ${line.join(", ")}`;
|
|
228
334
|
}
|
|
229
335
|
|
|
230
|
-
p.__piRogueContextBroker = {
|
|
231
|
-
renderBrief: currentBrief,
|
|
232
|
-
lookup: broker.lookup,
|
|
233
|
-
status: broker.status,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
336
|
function publishToolArtifact(event: {
|
|
237
337
|
toolName: string;
|
|
238
338
|
input?: any;
|
|
@@ -265,18 +365,25 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
265
365
|
};
|
|
266
366
|
const payload = toolPayload(sanitizedEvent);
|
|
267
367
|
const bytes = Buffer.byteLength(payload, "utf8");
|
|
368
|
+
const hostilePayload = isHostilePayload(payload) || hasHostileValue(sanitizedEvent);
|
|
268
369
|
const artifact = broker.publish({
|
|
269
370
|
sessionId: activeSessionId,
|
|
270
371
|
kind: "tool_output",
|
|
271
372
|
payload,
|
|
272
|
-
summary: summarizeTool(sanitizedEvent, bytes),
|
|
273
|
-
tags: [
|
|
373
|
+
summary: summarizeTool(sanitizedEvent, bytes, hostilePayload),
|
|
374
|
+
tags: [
|
|
375
|
+
event.toolName,
|
|
376
|
+
event.isError ? "error" : "ok",
|
|
377
|
+
event.sourceId ? "session-backfill" : "live",
|
|
378
|
+
...(hostilePayload ? ["hostile", "binary"] : []),
|
|
379
|
+
],
|
|
274
380
|
command: event.toolName === "bash" && typeof sanitizedEvent.input?.command === "string" ? sanitizedEvent.input.command : undefined,
|
|
275
381
|
paths: typeof sanitizedEvent.input?.path === "string" ? [sanitizedEvent.input.path] : [],
|
|
276
382
|
ttlMs: event.ttlMs ?? DEFAULT_TTL_MS,
|
|
277
383
|
parentIds: event.sourceId ? [event.sourceId] : [],
|
|
278
384
|
createdAt: event.createdAt,
|
|
279
385
|
});
|
|
386
|
+
if (artifact) routingTelemetry.toolResultArtifacts += 1;
|
|
280
387
|
if (event.sourceId) sourceHandles.set(event.sourceId, artifact.handle);
|
|
281
388
|
return artifact;
|
|
282
389
|
}
|
|
@@ -317,6 +424,7 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
317
424
|
|
|
318
425
|
if (entry?.type === "message" && entry.message?.role === "toolResult") {
|
|
319
426
|
scanned += 1;
|
|
427
|
+
routingTelemetry.backfillScans += 1;
|
|
320
428
|
const sourceId = typeof entry.message.toolCallId === "string" ? entry.message.toolCallId : entryId;
|
|
321
429
|
const toolInput = sourceId ? toolInputs.get(sourceId) : undefined;
|
|
322
430
|
const alreadySeen = sourceId ? seenSourceIds.has(sourceId) || sourceHandles.has(sourceId) : false;
|
|
@@ -329,12 +437,16 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
329
437
|
sourceId,
|
|
330
438
|
createdAt,
|
|
331
439
|
ttlMs: ttlFromNowFor(createdAt),
|
|
332
|
-
}) && !alreadySeen)
|
|
440
|
+
}) && !alreadySeen) {
|
|
441
|
+
added += 1;
|
|
442
|
+
routingTelemetry.backfillAdded += 1;
|
|
443
|
+
}
|
|
333
444
|
}
|
|
334
445
|
|
|
335
446
|
if (entry?.type === "message" && entry.message?.role === "bashExecution") {
|
|
336
447
|
if (entry.message.excludeFromContext === true) continue;
|
|
337
448
|
scanned += 1;
|
|
449
|
+
routingTelemetry.backfillScans += 1;
|
|
338
450
|
const sourceId = entryId;
|
|
339
451
|
const alreadySeen = sourceId ? seenSourceIds.has(sourceId) || sourceHandles.has(sourceId) : false;
|
|
340
452
|
if (publishToolArtifact({
|
|
@@ -351,16 +463,30 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
351
463
|
sourceId,
|
|
352
464
|
createdAt,
|
|
353
465
|
ttlMs: ttlFromNowFor(createdAt),
|
|
354
|
-
}) && !alreadySeen)
|
|
466
|
+
}) && !alreadySeen) {
|
|
467
|
+
added += 1;
|
|
468
|
+
routingTelemetry.backfillAdded += 1;
|
|
469
|
+
}
|
|
355
470
|
}
|
|
356
471
|
} catch {
|
|
357
472
|
errors += 1;
|
|
473
|
+
routingTelemetry.backfillErrors += 1;
|
|
358
474
|
}
|
|
359
475
|
}
|
|
360
476
|
|
|
361
477
|
return { added, scanned, errors };
|
|
362
478
|
}
|
|
363
479
|
|
|
480
|
+
function currentBrief(): string {
|
|
481
|
+
return broker.renderBrief({ sessionId: activeSessionId, budgetBytes: briefBytes });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
p.__piRogueContextBroker = {
|
|
485
|
+
renderBrief: currentBrief,
|
|
486
|
+
lookup: broker.lookup,
|
|
487
|
+
status: broker.status,
|
|
488
|
+
};
|
|
489
|
+
|
|
364
490
|
const contextActions: AutocompleteItem[] = [
|
|
365
491
|
{ value: "status", label: "status", description: "Show broker record, byte, and pinned counts" },
|
|
366
492
|
{ value: "brief", label: "brief", description: "Show the bounded broker brief" },
|
|
@@ -408,9 +534,9 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
408
534
|
|
|
409
535
|
pi.on("session_start", async (_event, ctx) => {
|
|
410
536
|
const { added, scanned, errors } = backfillSessionArtifacts(ctx);
|
|
411
|
-
ctx.ui.setStatus?.("context-broker", "ctx:on
|
|
537
|
+
ctx.ui.setStatus?.("context-broker", "ctx:on");
|
|
412
538
|
ctx.ui.notify(
|
|
413
|
-
`Context broker
|
|
539
|
+
`Context broker enabled. Backfilled ${added}/${scanned} current-branch tool artifacts${errors ? ` (${errors} malformed skipped)` : ""}. Use /context status or /context brief.`,
|
|
414
540
|
errors ? "warning" : "info",
|
|
415
541
|
);
|
|
416
542
|
});
|
|
@@ -427,18 +553,24 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
427
553
|
|
|
428
554
|
pi.on("tool_result", async (event: ToolResultEvent, ctx) => {
|
|
429
555
|
activeSessionId = sessionIdFor(ctx);
|
|
556
|
+
routingTelemetry.toolResultEvents += 1;
|
|
430
557
|
publishToolArtifact({ ...event, sourceId: event.toolCallId });
|
|
431
558
|
});
|
|
432
559
|
|
|
433
560
|
pi.on("context", async (event, ctx) => {
|
|
434
561
|
activeSessionId = sessionIdFor(ctx);
|
|
562
|
+
routingTelemetry.contextHookCalls += 1;
|
|
435
563
|
const toolInputs = collectToolInputs(event.messages);
|
|
436
|
-
const drafts = event.messages.map((message: any): { original: any; replacement?: any; artifact?: ContextArtifact; rewrite?: (artifact: ContextArtifact) => any } => {
|
|
564
|
+
const drafts = event.messages.map((message: any): { original: any; replacement?: any; artifact?: ContextArtifact; rewrite?: (artifact: ContextArtifact) => any; safeFallback?: any } => {
|
|
437
565
|
if (message?.role === "toolResult") {
|
|
566
|
+
routingTelemetry.contextHookToolResults += 1;
|
|
438
567
|
const raw = contentText(message.content);
|
|
439
|
-
if (Buffer.byteLength(raw, "utf8") <= rewriteThresholdBytes) return { original: message };
|
|
440
568
|
const toolInput = typeof message.toolCallId === "string" ? toolInputs.get(message.toolCallId) : undefined;
|
|
441
569
|
const toolName = String(message.toolName ?? toolInput?.toolName ?? "tool");
|
|
570
|
+
const hostile = hasHostileText(raw) || hasHostileValue(message.content);
|
|
571
|
+
if (hostile) routingTelemetry.contextHookToolResultHostile += 1;
|
|
572
|
+
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
573
|
+
if (!shouldRewrite) return { original: message };
|
|
442
574
|
if (!shouldBrokerToolName(toolName)) {
|
|
443
575
|
return { original: message, replacement: { ...message, content: [{ type: "text", text: contextLookupHistoryPlaceholder() }] } };
|
|
444
576
|
}
|
|
@@ -453,12 +585,22 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
453
585
|
ttlMs: ttlFromNowFor(typeof message.timestamp === "number" ? message.timestamp : undefined),
|
|
454
586
|
});
|
|
455
587
|
if (!artifact) return { original: message };
|
|
456
|
-
|
|
588
|
+
routingTelemetry.contextHookToolResultRewrites += 1;
|
|
589
|
+
return {
|
|
590
|
+
original: message,
|
|
591
|
+
artifact,
|
|
592
|
+
rewrite: (live) => ({ ...message, content: [{ type: "text", text: brokerPlaceholder(live) }] }),
|
|
593
|
+
safeFallback: { ...message, content: [{ type: "text", text: prunedPayloadPlaceholder(hostile) }] },
|
|
594
|
+
};
|
|
457
595
|
}
|
|
458
596
|
|
|
459
597
|
if (message?.role === "bashExecution" && message.excludeFromContext !== true) {
|
|
598
|
+
routingTelemetry.contextHookBash += 1;
|
|
460
599
|
const raw = String(message.output ?? "");
|
|
461
|
-
|
|
600
|
+
const hostile = hasHostileText(raw) || hasHostileValue(message.output);
|
|
601
|
+
if (hostile) routingTelemetry.contextHookBashHostile += 1;
|
|
602
|
+
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
603
|
+
if (!shouldRewrite) return { original: message };
|
|
462
604
|
const sourceId = typeof message.timestamp === "number"
|
|
463
605
|
? `bash:${message.timestamp}:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`
|
|
464
606
|
: `bash:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`;
|
|
@@ -478,7 +620,13 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
478
620
|
ttlMs: ttlFromNowFor(typeof message.timestamp === "number" ? message.timestamp : undefined),
|
|
479
621
|
});
|
|
480
622
|
if (!artifact) return { original: message };
|
|
481
|
-
|
|
623
|
+
routingTelemetry.contextHookBashRewrites += 1;
|
|
624
|
+
return {
|
|
625
|
+
original: message,
|
|
626
|
+
artifact,
|
|
627
|
+
rewrite: (live) => ({ ...message, output: brokerPlaceholder(live), truncated: true }),
|
|
628
|
+
safeFallback: { ...message, output: prunedPayloadPlaceholder(hostile), truncated: true },
|
|
629
|
+
};
|
|
482
630
|
}
|
|
483
631
|
|
|
484
632
|
return { original: message };
|
|
@@ -494,6 +642,10 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
494
642
|
const live = broker.lookup({ handle: draft.artifact.handle })[0];
|
|
495
643
|
if (!live) {
|
|
496
644
|
for (const parentId of draft.artifact.parentIds) sourceHandles.delete(parentId);
|
|
645
|
+
if (draft.safeFallback) {
|
|
646
|
+
changed = true;
|
|
647
|
+
return draft.safeFallback;
|
|
648
|
+
}
|
|
497
649
|
return draft.original;
|
|
498
650
|
}
|
|
499
651
|
changed = true;
|
|
@@ -510,7 +662,7 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
510
662
|
systemPrompt: [
|
|
511
663
|
event.systemPrompt,
|
|
512
664
|
brief,
|
|
513
|
-
"Context broker
|
|
665
|
+
"Context broker rule: use /context lookup <handle> for exact evidence when a broker handle is relevant. Broker briefs are bounded summaries and never raw payload dumps.",
|
|
514
666
|
].join("\n\n"),
|
|
515
667
|
};
|
|
516
668
|
});
|
|
@@ -535,8 +687,11 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
535
687
|
}),
|
|
536
688
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
537
689
|
activeSessionId = sessionIdFor(ctx);
|
|
690
|
+
routingTelemetry.toolLookupCalls += 1;
|
|
538
691
|
const p = params as { handle?: string; text?: string; path?: string; tag?: string; kind?: any; tier?: any; limit?: number };
|
|
539
692
|
const exact = typeof p.handle === "string" && p.handle.startsWith("ctx://");
|
|
693
|
+
routingTelemetry.toolLookupExactCalls += exact ? 1 : 0;
|
|
694
|
+
routingTelemetry.toolLookupTextCalls += exact ? 0 : 1;
|
|
540
695
|
const focused = exact || Boolean(p.text?.trim() || p.path?.trim() || p.tag?.trim() || p.kind || p.tier);
|
|
541
696
|
if (!focused) {
|
|
542
697
|
return textResult("context_lookup requires a focused filter: handle, text, path, tag, kind, or tier. Empty lookups are refused to avoid dumping brokered payloads into the prompt.");
|
|
@@ -551,13 +706,17 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
551
706
|
tier: p.tier,
|
|
552
707
|
limit: Math.min(10, Math.max(1, Math.floor(p.limit ?? (exact ? 1 : 5)))),
|
|
553
708
|
});
|
|
554
|
-
if (!results.length)
|
|
709
|
+
if (!results.length) {
|
|
710
|
+
routingTelemetry.toolLookupMisses += 1;
|
|
711
|
+
return textResult("No context artifacts matched. Missing or expired handles should be reported explicitly.");
|
|
712
|
+
}
|
|
713
|
+
routingTelemetry.toolLookupHits += 1;
|
|
555
714
|
return textResult(results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n"));
|
|
556
715
|
},
|
|
557
716
|
});
|
|
558
717
|
|
|
559
718
|
pi.registerCommand("context", {
|
|
560
|
-
description: "Inspect the
|
|
719
|
+
description: "Inspect the context broker: status | brief | lookup <handle-or-text> | pin <handle-or-id> | export <handle-or-id> | prune",
|
|
561
720
|
getArgumentCompletions: contextArgumentCompletions,
|
|
562
721
|
handler: async (args, ctx) => {
|
|
563
722
|
activeSessionId = sessionIdFor(ctx);
|
|
@@ -565,11 +724,13 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
565
724
|
const query = rest.join(" ");
|
|
566
725
|
|
|
567
726
|
if (action === "status") {
|
|
727
|
+
routingTelemetry.statusCalls += 1;
|
|
568
728
|
const status = broker.status();
|
|
569
729
|
ctx.ui.notify(
|
|
570
|
-
`Context broker
|
|
730
|
+
`Context broker: enabled, session=${activeSessionId}, records=${status.records}, bytes=${status.bytes}/${status.maxBytes}, tiers=hot:${status.hotRecords}/${status.hotBytes} warm:${status.warmRecords}/${status.warmBytes} cold:${status.coldRecords}/${status.coldBytes}, pinned=${status.pinnedRecords}/${status.pinnedBytes} bytes`,
|
|
571
731
|
"info",
|
|
572
732
|
);
|
|
733
|
+
ctx.ui.notify(formatRoutingTelemetry(), "info");
|
|
573
734
|
return;
|
|
574
735
|
}
|
|
575
736
|
|
|
@@ -583,8 +744,16 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
583
744
|
ctx.ui.notify("Usage: /context lookup <ctx://handle-or-text>", "warning");
|
|
584
745
|
return;
|
|
585
746
|
}
|
|
747
|
+
routingTelemetry.commandLookupCalls += 1;
|
|
586
748
|
const exact = query.startsWith("ctx://");
|
|
749
|
+
routingTelemetry.commandLookupExactCalls += exact ? 1 : 0;
|
|
750
|
+
routingTelemetry.commandLookupTextCalls += exact ? 0 : 1;
|
|
587
751
|
const results = broker.lookup(exact ? { handle: query } : { sessionId: activeSessionId, text: query, limit: 5 });
|
|
752
|
+
if (results.length) {
|
|
753
|
+
routingTelemetry.commandLookupHits += 1;
|
|
754
|
+
} else {
|
|
755
|
+
routingTelemetry.commandLookupMisses += 1;
|
|
756
|
+
}
|
|
588
757
|
ctx.ui.notify(results.length ? results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n") : "No context artifacts matched.", "info");
|
|
589
758
|
return;
|
|
590
759
|
}
|
|
@@ -595,6 +764,7 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
595
764
|
return;
|
|
596
765
|
}
|
|
597
766
|
const pinned = broker.pin(query, true);
|
|
767
|
+
routingTelemetry.pinCalls += 1;
|
|
598
768
|
ctx.ui.notify(pinned ? `Pinned ${pinned.handle}` : "No artifact matched that handle/id.", pinned ? "info" : "warning");
|
|
599
769
|
return;
|
|
600
770
|
}
|
|
@@ -608,18 +778,20 @@ export function registerContextBrokerBeta(pi: ExtensionAPI, options: ContextBrok
|
|
|
608
778
|
const exact = query.startsWith("ctx://");
|
|
609
779
|
const artifact = exact ? broker.lookup({ handle: query })[0] : broker.lookup({ id: query })[0];
|
|
610
780
|
if (!artifact) {
|
|
611
|
-
ctx.ui.notify("No artifact matched that handle
|
|
781
|
+
ctx.ui.notify("No artifact matched that handle-or-id.", "warning");
|
|
612
782
|
return;
|
|
613
783
|
}
|
|
614
784
|
|
|
615
785
|
const exportDir = mkdtempSync(join(tmpdir(), "pi-context-broker-export-"));
|
|
616
786
|
const exportPath = join(exportDir, `${artifact.id}.txt`);
|
|
617
787
|
writeFileSync(exportPath, artifact.payload, "utf8");
|
|
788
|
+
routingTelemetry.exportCalls += 1;
|
|
618
789
|
ctx.ui.notify(`Exported full payload for ${sanitizeForPrompt(artifact.handle)} (${artifact.bytes} bytes) to ${exportPath}`, "info");
|
|
619
790
|
return;
|
|
620
791
|
}
|
|
621
792
|
|
|
622
793
|
if (action === "prune") {
|
|
794
|
+
routingTelemetry.pruneCalls += 1;
|
|
623
795
|
const status = broker.prune();
|
|
624
796
|
ctx.ui.notify(`Pruned. ${status.records} records, ${status.bytes} bytes remain.`, "info");
|
|
625
797
|
return;
|
|
@@ -287,4 +287,16 @@ describe("createInMemoryContextBroker", () => {
|
|
|
287
287
|
expect(broker.lookup({ handle: pinned.handle })[0]?.payload).toBe("keep");
|
|
288
288
|
expect(broker.lookup({ handle: other.handle })[0]?.payload).toBe("other");
|
|
289
289
|
});
|
|
290
|
+
|
|
291
|
+
it("enforces optional global caps across sessions", () => {
|
|
292
|
+
const broker = createInMemoryContextBroker({ maxRecords: 8, globalMaxRecords: 2 });
|
|
293
|
+
const first = broker.publish({ sessionId: "s1", kind: "tool_output", payload: "alpha", summary: "alpha" });
|
|
294
|
+
const second = broker.publish({ sessionId: "s2", kind: "tool_output", payload: "bravo", summary: "bravo" });
|
|
295
|
+
const pinned = broker.publish({ sessionId: "s3", kind: "tool_output", payload: "charlie", summary: "charlie", pinned: true });
|
|
296
|
+
broker.publish({ sessionId: "s1", kind: "tool_output", payload: "delta", summary: "delta" });
|
|
297
|
+
|
|
298
|
+
expect(broker.lookup({ handle: first.handle })).toEqual([]);
|
|
299
|
+
expect(broker.lookup({ handle: second.handle })).toEqual([]);
|
|
300
|
+
expect(broker.lookup({ handle: pinned.handle })[0]?.payload).toBe("charlie");
|
|
301
|
+
});
|
|
290
302
|
});
|
|
@@ -127,6 +127,12 @@ function tierLine(artifact: ContextArtifact): string {
|
|
|
127
127
|
export function createInMemoryContextBroker(options: ContextBrokerOptions = {}): BoundedContextBroker {
|
|
128
128
|
const maxRecords = Math.max(1, Math.floor(options.maxRecords ?? DEFAULT_MAX_RECORDS));
|
|
129
129
|
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
130
|
+
const globalMaxRecords = typeof options.globalMaxRecords === "number" && Number.isFinite(options.globalMaxRecords)
|
|
131
|
+
? Math.max(1, Math.floor(options.globalMaxRecords))
|
|
132
|
+
: Number.POSITIVE_INFINITY;
|
|
133
|
+
const globalMaxBytes = typeof options.globalMaxBytes === "number" && Number.isFinite(options.globalMaxBytes)
|
|
134
|
+
? Math.max(1, Math.floor(options.globalMaxBytes))
|
|
135
|
+
: Number.POSITIVE_INFINITY;
|
|
130
136
|
const defaultTtlMs = Math.max(0, Math.floor(options.defaultTtlMs ?? DEFAULT_TTL_MS));
|
|
131
137
|
const tierTtlMs: Record<ContextArtifactTier, number> = {
|
|
132
138
|
hot: Math.max(0, Math.floor(options.hotTtlMs ?? defaultTtlMs)),
|
|
@@ -190,6 +196,19 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
190
196
|
});
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
function removalCandidatesGlobal(protectedIds: Set<string>, tier?: ContextArtifactTier): Array<{ artifact: ContextArtifact & { sequence: number; baseTier: ContextArtifactTier }; index: number }> {
|
|
200
|
+
return artifacts
|
|
201
|
+
.map((artifact, index) => ({ artifact, index }))
|
|
202
|
+
.filter(({ artifact }) => !artifact.pinned && !protectedIds.has(artifact.id) && (!tier || artifact.tier === tier))
|
|
203
|
+
.sort((a, b) => {
|
|
204
|
+
if (!tier && TIER_REMOVAL_ORDER[a.artifact.tier] !== TIER_REMOVAL_ORDER[b.artifact.tier]) {
|
|
205
|
+
return TIER_REMOVAL_ORDER[a.artifact.tier] - TIER_REMOVAL_ORDER[b.artifact.tier];
|
|
206
|
+
}
|
|
207
|
+
if (a.artifact.createdAt !== b.artifact.createdAt) return a.artifact.createdAt - b.artifact.createdAt;
|
|
208
|
+
return a.artifact.sequence - b.artifact.sequence;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
function withinCaps(sessionId: string, tier?: ContextArtifactTier): boolean {
|
|
194
213
|
const sessionArtifacts = artifacts.filter((artifact) => artifact.sessionId === sessionId && (!tier || artifact.tier === tier));
|
|
195
214
|
const recordsCap = tier ? tierMaxRecords[tier] : maxRecords;
|
|
@@ -197,6 +216,13 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
197
216
|
return sessionArtifacts.length <= recordsCap && sessionArtifacts.reduce((sum, artifact) => sum + artifact.bytes, 0) <= bytesCap;
|
|
198
217
|
}
|
|
199
218
|
|
|
219
|
+
function withinGlobalCaps(): boolean {
|
|
220
|
+
if (globalMaxRecords === Number.POSITIVE_INFINITY && globalMaxBytes === Number.POSITIVE_INFINITY) return true;
|
|
221
|
+
const records = artifacts.length;
|
|
222
|
+
const bytes = artifacts.reduce((sum, artifact) => sum + artifact.bytes, 0);
|
|
223
|
+
return records <= globalMaxRecords && bytes <= globalMaxBytes;
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
|
|
201
227
|
dropExpired(now, protectedIds);
|
|
202
228
|
|
|
@@ -216,6 +242,12 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
216
242
|
}
|
|
217
243
|
}
|
|
218
244
|
|
|
245
|
+
while (!withinGlobalCaps()) {
|
|
246
|
+
const candidate = removalCandidatesGlobal(protectedIds)[0];
|
|
247
|
+
if (!candidate) break;
|
|
248
|
+
artifacts.splice(candidate.index, 1);
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
return currentStatus();
|
|
220
252
|
}
|
|
221
253
|
|
|
@@ -95,4 +95,28 @@ describe("createSqliteContextBroker", () => {
|
|
|
95
95
|
rmSync(dir, { recursive: true, force: true });
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it("enforces optional global caps across sessions", () => {
|
|
100
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
101
|
+
try {
|
|
102
|
+
const path = join(dir, "artifacts.sqlite");
|
|
103
|
+
const broker = createSqliteContextBroker({
|
|
104
|
+
path,
|
|
105
|
+
defaultTtlMs: 0,
|
|
106
|
+
globalMaxBytes: 10,
|
|
107
|
+
globalMaxRecords: Number.POSITIVE_INFINITY,
|
|
108
|
+
});
|
|
109
|
+
const one = broker.publish({ sessionId: "s1", kind: "tool_output", payload: "aaa", summary: "first" });
|
|
110
|
+
const two = broker.publish({ sessionId: "s2", kind: "tool_output", payload: "bbb", summary: "second" });
|
|
111
|
+
const pinned = broker.publish({ sessionId: "s3", kind: "tool_output", payload: "ccc", summary: "third", pinned: true });
|
|
112
|
+
const four = broker.publish({ sessionId: "s1", kind: "tool_output", payload: "ddd", summary: "fourth" });
|
|
113
|
+
|
|
114
|
+
expect(broker.lookup({ handle: one.handle })).toEqual([]);
|
|
115
|
+
expect(broker.lookup({ handle: two.handle })[0]?.payload).toBe("bbb");
|
|
116
|
+
expect(broker.lookup({ handle: pinned.handle })[0]?.payload).toBe("ccc");
|
|
117
|
+
expect(broker.lookup({ handle: four.handle })[0]?.payload).toBe("ddd");
|
|
118
|
+
} finally {
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
98
122
|
});
|
|
@@ -204,6 +204,12 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
204
204
|
|
|
205
205
|
const maxRecords = Math.max(1, Math.floor(options.maxRecords ?? DEFAULT_MAX_RECORDS));
|
|
206
206
|
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
207
|
+
const globalMaxRecords = typeof options.globalMaxRecords === "number" && Number.isFinite(options.globalMaxRecords)
|
|
208
|
+
? Math.max(1, Math.floor(options.globalMaxRecords))
|
|
209
|
+
: Number.POSITIVE_INFINITY;
|
|
210
|
+
const globalMaxBytes = typeof options.globalMaxBytes === "number" && Number.isFinite(options.globalMaxBytes)
|
|
211
|
+
? Math.max(1, Math.floor(options.globalMaxBytes))
|
|
212
|
+
: Number.POSITIVE_INFINITY;
|
|
207
213
|
const defaultTtlMs = Math.max(0, Math.floor(options.defaultTtlMs ?? DEFAULT_TTL_MS));
|
|
208
214
|
const tierTtlMs: Record<ContextArtifactTier, number> = {
|
|
209
215
|
hot: Math.max(0, Math.floor(options.hotTtlMs ?? defaultTtlMs)),
|
|
@@ -286,6 +292,17 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
286
292
|
return stats.records <= (tier ? tierMaxRecords[tier] : maxRecords) && stats.bytes <= (tier ? tierMaxBytes[tier] : maxBytes);
|
|
287
293
|
}
|
|
288
294
|
|
|
295
|
+
function globalStats(): { records: number; bytes: number } {
|
|
296
|
+
const row = db.prepare("SELECT COUNT(*) AS records, COALESCE(SUM(bytes), 0) AS bytes FROM artifacts").get();
|
|
297
|
+
return { records: Number(row?.records ?? 0), bytes: Number(row?.bytes ?? 0) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function withinGlobalCaps(): boolean {
|
|
301
|
+
if (globalMaxRecords === Number.POSITIVE_INFINITY && globalMaxBytes === Number.POSITIVE_INFINITY) return true;
|
|
302
|
+
const { records, bytes } = globalStats();
|
|
303
|
+
return records <= globalMaxRecords && bytes <= globalMaxBytes;
|
|
304
|
+
}
|
|
305
|
+
|
|
289
306
|
function removalCandidate(sessionId: string, protectedIds: Set<string>, tier?: ContextArtifactTier): string | undefined {
|
|
290
307
|
const protectedList = [...protectedIds];
|
|
291
308
|
const protectedClause = protectedList.length ? `AND id NOT IN (${protectedList.map(() => "?").join(",")})` : "";
|
|
@@ -296,6 +313,20 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
296
313
|
return row?.id == null ? undefined : String(row.id);
|
|
297
314
|
}
|
|
298
315
|
|
|
316
|
+
function removalCandidateGlobal(protectedIds: Set<string>, tier?: ContextArtifactTier): string | undefined {
|
|
317
|
+
const protectedList = [...protectedIds];
|
|
318
|
+
const protectedClause = protectedList.length ? `AND id NOT IN (${protectedList.map(() => "?").join(",")})` : "";
|
|
319
|
+
const tierClause = tier ? "AND tier = ?" : "";
|
|
320
|
+
const order = tier
|
|
321
|
+
? "createdAt ASC, rowid ASC"
|
|
322
|
+
: "CASE tier WHEN 'cold' THEN 0 WHEN 'warm' THEN 1 ELSE 2 END ASC, createdAt ASC, rowid ASC";
|
|
323
|
+
const params = tier ? [tier, ...protectedList] : [...protectedList];
|
|
324
|
+
const row = db.prepare(
|
|
325
|
+
`SELECT id FROM artifacts WHERE pinned = 0 ${tierClause} ${protectedClause} ORDER BY ${order} LIMIT 1`,
|
|
326
|
+
).get(...params);
|
|
327
|
+
return row?.id == null ? undefined : String(row.id);
|
|
328
|
+
}
|
|
329
|
+
|
|
299
330
|
function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
|
|
300
331
|
dropExpired(now, protectedIds);
|
|
301
332
|
const sessions = db.prepare("SELECT DISTINCT sessionId FROM artifacts").all().map((row) => String(row.sessionId));
|
|
@@ -314,6 +345,12 @@ export function createSqliteContextBroker(options: SqliteContextBrokerOptions =
|
|
|
314
345
|
deleteArtifact(id);
|
|
315
346
|
}
|
|
316
347
|
}
|
|
348
|
+
|
|
349
|
+
while (!withinGlobalCaps()) {
|
|
350
|
+
const id = removalCandidateGlobal(protectedIds);
|
|
351
|
+
if (!id) break;
|
|
352
|
+
deleteArtifact(id);
|
|
353
|
+
}
|
|
317
354
|
return currentStatus();
|
|
318
355
|
}
|
|
319
356
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiale-plus/pi-rogue-bundle",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Public Pi-Rogue bundle for advisor, orchestration, and
|
|
3
|
+
"version": "0.1.20",
|
|
4
|
+
"description": "Public Pi-Rogue bundle for advisor, orchestration, and context broker. Single consolidated artefact (leaf releases paused; private packages are bundled here).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
package/src/extension.test.ts
CHANGED
|
@@ -33,27 +33,27 @@ describe("bundle extension defaults", () => {
|
|
|
33
33
|
else process.env.PI_CONTEXT_BROKER_ENABLED = oldEnv;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it("
|
|
36
|
+
it("registers the context broker by default", async () => {
|
|
37
37
|
delete process.env.PI_CONTEXT_BROKER_ENABLED;
|
|
38
38
|
const { pi, commands } = createPiMock();
|
|
39
39
|
|
|
40
40
|
await registerBundle(pi);
|
|
41
41
|
|
|
42
|
-
expect(commands.has("context")).toBe(
|
|
42
|
+
expect(commands.has("context")).toBe(true);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("
|
|
46
|
-
process.env.PI_CONTEXT_BROKER_ENABLED = "
|
|
45
|
+
it("keeps an explicit env kill switch for context broker rollout", async () => {
|
|
46
|
+
process.env.PI_CONTEXT_BROKER_ENABLED = "false";
|
|
47
47
|
const { pi, commands } = createPiMock();
|
|
48
48
|
|
|
49
49
|
await registerBundle(pi);
|
|
50
50
|
|
|
51
|
-
expect(commands.has("context")).toBe(
|
|
51
|
+
expect(commands.has("context")).toBe(false);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
describe("bundle context-broker export", () => {
|
|
56
|
-
it("exposes the
|
|
56
|
+
it("exposes the context broker runtime through a bundle subpath", () => {
|
|
57
57
|
const broker = createInMemoryContextBroker({ defaultTtlMs: 0 });
|
|
58
58
|
const artifact = broker.publish({ sessionId: "bundle-test", kind: "memory_note", payload: "hello" });
|
|
59
59
|
|
package/src/extension.ts
CHANGED
|
@@ -2,10 +2,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { registerAdvisor } from "@fiale-plus/pi-rogue-advisor";
|
|
3
3
|
import { registerOrchestration } from "@fiale-plus/pi-rogue-orchestration";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DISABLED_VALUES = new Set(["0", "false", "no", "off"]);
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
return
|
|
7
|
+
function contextBrokerEnabled(): boolean {
|
|
8
|
+
return !DISABLED_VALUES.has(String(process.env.PI_CONTEXT_BROKER_ENABLED ?? "").trim().toLowerCase());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export async function registerBundle(pi: ExtensionAPI): Promise<void> {
|
|
@@ -13,9 +13,9 @@ export async function registerBundle(pi: ExtensionAPI): Promise<void> {
|
|
|
13
13
|
if (p.__piRogueBundleRegistered) return;
|
|
14
14
|
p.__piRogueBundleRegistered = true;
|
|
15
15
|
|
|
16
|
-
if (
|
|
16
|
+
if (contextBrokerEnabled()) {
|
|
17
17
|
const { registerContextBrokerBeta } = await import("@fiale-plus/pi-rogue-context-broker/extension");
|
|
18
|
-
registerContextBrokerBeta(pi);
|
|
18
|
+
await registerContextBrokerBeta(pi);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
registerAdvisor(pi);
|