@fiale-plus/pi-rogue 0.2.1 → 0.2.3
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 +2 -1
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +4 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +24 -5
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +119 -7
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +124 -16
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +32 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +32 -1
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +37 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +39 -2
- package/node_modules/@fiale-plus/pi-rogue-router/README.md +34 -0
- package/node_modules/@fiale-plus/pi-rogue-router/package.json +30 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.test.ts +84 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/checkpoints.ts +363 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +277 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +34 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +165 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +193 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/dataset.ts +154 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision-ledger.test.ts +148 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/decision.ts +138 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/git-features.ts +134 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/hash.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +15 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.test.ts +241 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/learning.ts +382 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/ledger.ts +94 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +128 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/progress.ts +93 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/session-reader.ts +217 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/subagents.ts +178 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/types.ts +150 -0
- package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +297 -0
- package/package.json +5 -3
- package/src/extension.test.ts +1 -0
- package/src/extension.ts +2 -0
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ It stitches together (and bundles for a true single-package install):
|
|
|
8
8
|
- `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
|
|
9
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
|
+
- `@fiale-plus/pi-rogue-router` (observe-only trajectory-router lab; direct releases paused)
|
|
11
12
|
|
|
12
13
|
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.
|
|
13
14
|
|
|
@@ -37,7 +38,7 @@ npm install
|
|
|
37
38
|
|
|
38
39
|
## Command surface
|
|
39
40
|
|
|
40
|
-
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
|
|
41
|
+
- Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab`, `/router` plus status/config/command paths (all provided via the bundle).
|
|
41
42
|
- 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
43
|
|
|
43
44
|
## Status
|
|
@@ -72,6 +72,8 @@ export interface ContextBrokerStatus {
|
|
|
72
72
|
coldBytes: number;
|
|
73
73
|
maxRecords: number;
|
|
74
74
|
maxBytes: number;
|
|
75
|
+
globalMaxRecords: number;
|
|
76
|
+
globalMaxBytes: number;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export interface ContextBrokerOptions {
|
|
@@ -89,6 +91,8 @@ export interface ContextBrokerOptions {
|
|
|
89
91
|
hotMaxBytes?: number;
|
|
90
92
|
warmMaxBytes?: number;
|
|
91
93
|
coldMaxBytes?: number;
|
|
94
|
+
hotToWarmMs?: number;
|
|
95
|
+
warmToColdMs?: number;
|
|
92
96
|
summaryBytes?: number;
|
|
93
97
|
briefBytes?: number;
|
|
94
98
|
}
|
|
@@ -8,6 +8,7 @@ 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
|
+
- Aging cools unpinned artifacts from hot to warm and from warm to cold; compaction remains cleanup/removal, not a separate cooling trigger.
|
|
11
12
|
- 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
13
|
|
|
13
14
|
It is registered by default in the bundle, with an explicit env kill switch.
|
|
@@ -22,20 +23,36 @@ PI_CONTEXT_BROKER_ENABLED=false pi
|
|
|
22
23
|
|
|
23
24
|
When active, the bundle registers:
|
|
24
25
|
|
|
25
|
-
- `/context status` — enabled state, record/byte counts, pinned counts, and
|
|
26
|
+
- `/context status` — enabled state, record/byte counts, pinned counts, routing telemetry, and prompt rewrite savings bytes.
|
|
26
27
|
- `/context brief` — bounded prompt-safe broker brief with handles and summaries.
|
|
27
28
|
- `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
|
|
28
29
|
- `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
|
|
29
30
|
- `/context export <handle>` — write full payload to a temp file without dumping it into prompt.
|
|
30
31
|
- `/context prune` — run TTL/cap pruning immediately.
|
|
31
32
|
|
|
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.
|
|
33
|
+
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. Exact-handle misses and text/filter misses use distinct messages, and `/context status` reports exact/text miss counters.
|
|
33
34
|
|
|
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.
|
|
35
|
+
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. Durable mode applies default global retention caps when env caps are not set: 2,048 records and 256 MiB across sessions.
|
|
35
36
|
|
|
36
|
-
- `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` controls when large `toolResult` / `bashExecution` payloads are rewritten in-context. The default is `
|
|
37
|
+
- `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` controls when large `toolResult` / `bashExecution` payloads are rewritten in-context. The default is `8192` bytes, so small tool evidence remains inline while larger outputs are replaced by handles.
|
|
38
|
+
- `PI_CONTEXT_BROKER_HOT_TO_WARM_MS` controls unpinned artifact cooling from hot to warm. The default is 2 hours.
|
|
39
|
+
- `PI_CONTEXT_BROKER_WARM_TO_COLD_MS` controls unpinned artifact cooling from warm/hot to cold. The default is 12 hours.
|
|
37
40
|
|
|
38
|
-
For
|
|
41
|
+
For more aggressive prompt reduction, set `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES=0`. For quieter sessions, set it to a higher value to only rewrite larger outputs.
|
|
42
|
+
|
|
43
|
+
## Tier lifecycle policy
|
|
44
|
+
|
|
45
|
+
- Publish-time classification remains deterministic: explicit `tier`, `hot`/`warm`/`cold` tags, error tags, completed/archive tags, and artifact kind choose the base tier.
|
|
46
|
+
- Cooling only retiers unpinned artifacts for prompt visibility and cap pressure; it does not delete payloads.
|
|
47
|
+
- Pinned artifacts stay hot and are retained through compaction cleanup.
|
|
48
|
+
- Cold artifacts remain retrievable by exact handle/search, but are excluded from the default prompt brief unless explicitly queried.
|
|
49
|
+
- `/compact` / `session_compact` cleanup purges unpinned artifacts for the session; cooling is age-based and separate from compaction cleanup.
|
|
50
|
+
|
|
51
|
+
## Payload display policy
|
|
52
|
+
|
|
53
|
+
- Hostile/binary payloads are unsafe or control-heavy. They are stored/exportable but omitted from prompt lookup output with `/context export` guidance.
|
|
54
|
+
- Opaque payloads are printable but low-value/high-token, such as large base64-like blobs, hex dumps, minified single-line output, or compressed-looking text. They are also stored/exportable but omitted from prompt lookup output with `/context export` guidance.
|
|
55
|
+
- Normal code, logs, and test output remain visible subject to normal byte clipping.
|
|
39
56
|
|
|
40
57
|
|
|
41
58
|
## Session behavior and limits
|
|
@@ -45,9 +62,11 @@ For quieter sessions, set `PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES` to a highe
|
|
|
45
62
|
- Without durable mode, restarting Pi loses broker state until the current branch is backfilled again.
|
|
46
63
|
- 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
64
|
- 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.
|
|
65
|
+
- Current-turn `context_lookup` results are left visible so the model can consume requested exact evidence once. Historical `context_lookup` results that already have a later assistant response are omitted from later prompt assembly to avoid recursive prompt growth.
|
|
48
66
|
- Pi `excludeFromContext` bash entries are not backfilled or rewritten into broker prompts.
|
|
49
67
|
- Basic secret redaction runs before broker storage and display for common token/password/API-key patterns.
|
|
50
68
|
- Optional global caps can be configured via env vars:
|
|
51
69
|
- `PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS`
|
|
52
70
|
- `PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES`
|
|
71
|
+
- Durable mode defaults to global caps of 2,048 records and 256 MiB when those env vars are unset; in-memory mode remains per-session capped unless global caps are explicitly provided.
|
|
53
72
|
- 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`.
|
|
@@ -287,6 +287,57 @@ lookupBytes: 80, searchBytes: 50,
|
|
|
287
287
|
expect(commandMessage).not.toContain("\u0000");
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
it("omits opaque printable payloads from lookup output and suggests export", async () => {
|
|
291
|
+
const { pi, handlers, commands } = createPiMock();
|
|
292
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
293
|
+
const { ctx, notifications } = createCtx();
|
|
294
|
+
const payload = "A".repeat(6000);
|
|
295
|
+
|
|
296
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
297
|
+
await runHandlers(handlers, "tool_result", {
|
|
298
|
+
type: "tool_result",
|
|
299
|
+
toolCallId: "call-opaque",
|
|
300
|
+
toolName: "bash",
|
|
301
|
+
input: { command: "printf opaque" },
|
|
302
|
+
content: [{ type: "text", text: payload }],
|
|
303
|
+
isError: false,
|
|
304
|
+
}, ctx);
|
|
305
|
+
|
|
306
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
307
|
+
const lookupHandle = lookupCompletion?.value.replace(/^lookup /, "");
|
|
308
|
+
|
|
309
|
+
await commands.get("context").handler(`lookup ${lookupHandle}`, ctx);
|
|
310
|
+
const commandMessage = notifications.at(-1)?.message ?? "";
|
|
311
|
+
expect(commandMessage).toContain("payload omitted from prompt because it appears opaque/high-token");
|
|
312
|
+
expect(commandMessage).toContain("/context export");
|
|
313
|
+
expect(commandMessage).not.toContain(payload.slice(0, 200));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("does not classify normal multiline code as opaque", async () => {
|
|
317
|
+
const { pi, handlers, commands } = createPiMock();
|
|
318
|
+
registerContextBrokerBeta(pi, { lookupBytes: 8000, rewriteThresholdBytes: 1 });
|
|
319
|
+
const { ctx, notifications } = createCtx();
|
|
320
|
+
const payload = Array.from({ length: 240 }, (_, index) => `export function fn${index}() { return ${index}; }`).join("\n");
|
|
321
|
+
|
|
322
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
323
|
+
await runHandlers(handlers, "tool_result", {
|
|
324
|
+
type: "tool_result",
|
|
325
|
+
toolCallId: "call-code",
|
|
326
|
+
toolName: "read",
|
|
327
|
+
input: { path: "src/generated.ts" },
|
|
328
|
+
content: [{ type: "text", text: payload }],
|
|
329
|
+
isError: false,
|
|
330
|
+
}, ctx);
|
|
331
|
+
|
|
332
|
+
const lookupCompletion = commands.get("context").getArgumentCompletions("lookup ")?.[0];
|
|
333
|
+
const lookupHandle = lookupCompletion?.value.replace(/^lookup /, "");
|
|
334
|
+
|
|
335
|
+
await commands.get("context").handler(`lookup ${lookupHandle}`, ctx);
|
|
336
|
+
const commandMessage = notifications.at(-1)?.message ?? "";
|
|
337
|
+
expect(commandMessage).toContain("export function fn0");
|
|
338
|
+
expect(commandMessage).not.toContain("payload omitted from prompt because it appears opaque/high-token");
|
|
339
|
+
});
|
|
340
|
+
|
|
290
341
|
it("text search lookup returns a smaller byte-clipped excerpt", async () => {
|
|
291
342
|
const { pi, handlers, commands } = createPiMock();
|
|
292
343
|
registerContextBrokerBeta(pi, {
|
|
@@ -360,6 +411,27 @@ lookupBytes: 500,
|
|
|
360
411
|
expect(result.content[0].text).toContain("exact evidence payload");
|
|
361
412
|
});
|
|
362
413
|
|
|
414
|
+
it("distinguishes exact-handle and text lookup misses", async () => {
|
|
415
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
416
|
+
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1 });
|
|
417
|
+
const { ctx, notifications } = createCtx();
|
|
418
|
+
|
|
419
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
420
|
+
const toolExactMiss = await tools.get("context_lookup").execute("lookup-missing-handle", { handle: "ctx://missing/handle" }, undefined, undefined, ctx);
|
|
421
|
+
const toolTextMiss = await tools.get("context_lookup").execute("lookup-missing-text", { text: "definitely absent" }, undefined, undefined, ctx);
|
|
422
|
+
await commands.get("context").handler("lookup ctx://missing/handle", ctx);
|
|
423
|
+
await commands.get("context").handler("lookup definitely absent", ctx);
|
|
424
|
+
await commands.get("context").handler("status", ctx);
|
|
425
|
+
|
|
426
|
+
expect(toolExactMiss.content[0].text).toContain("exact handle");
|
|
427
|
+
expect(toolTextMiss.content[0].text).toContain("text/filter query");
|
|
428
|
+
expect(notifications.at(-4)?.message).toContain("exact handle");
|
|
429
|
+
expect(notifications.at(-3)?.message).toContain("text/filter query");
|
|
430
|
+
const telemetry = notifications.at(-1)?.message ?? "";
|
|
431
|
+
expect(telemetry).toContain("exactMisses=1");
|
|
432
|
+
expect(telemetry).toContain("textMisses=1");
|
|
433
|
+
});
|
|
434
|
+
|
|
363
435
|
it("reports routing telemetry in /context status", async () => {
|
|
364
436
|
const { pi, handlers, commands, tools } = createPiMock();
|
|
365
437
|
registerContextBrokerBeta(pi, { rewriteThresholdBytes: 1, lookupBytes: 500, searchBytes: 500 });
|
|
@@ -382,7 +454,7 @@ lookupBytes: 500,
|
|
|
382
454
|
await commands.get("context").handler(`export ${handle}`, ctx);
|
|
383
455
|
const result = await handlers.get("context")?.[0]({
|
|
384
456
|
type: "context",
|
|
385
|
-
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(
|
|
457
|
+
messages: [{ role: "toolResult", toolCallId: "tool-result-telemetry", toolName: "bash", content: [{ type: "text", text: "telemetry_payload_" + "y".repeat(1000) }], isError: false, timestamp: 1 }],
|
|
386
458
|
}, ctx);
|
|
387
459
|
|
|
388
460
|
await commands.get("context").handler("status", ctx);
|
|
@@ -390,17 +462,52 @@ lookupBytes: 500,
|
|
|
390
462
|
expect(handle).toBeTruthy();
|
|
391
463
|
expect(toolResult.content[0].text).toContain("telemetry_payload_");
|
|
392
464
|
expect(result).toBeDefined();
|
|
465
|
+
const statusMessage = notifications.at(-2)?.message ?? "";
|
|
466
|
+
expect(statusMessage).toContain("globalCaps=records:unbounded bytes:unbounded");
|
|
393
467
|
const telemetry = notifications.at(-1)?.message ?? "";
|
|
394
468
|
expect(telemetry).toContain("Context broker routing telemetry:");
|
|
469
|
+
expect(telemetry).toContain("rewriteSavings rawBytes=");
|
|
470
|
+
expect(telemetry).toContain("replacementBytes=");
|
|
471
|
+
expect(telemetry).toContain("savedBytes=");
|
|
472
|
+
expect(telemetry).toMatch(/savedBytes=[1-9]\d*/);
|
|
473
|
+
expect(telemetry).toContain("contextLookupHistoryOmitted=");
|
|
395
474
|
expect(telemetry).toContain("lookups tool(calls=");
|
|
475
|
+
expect(telemetry).toContain("exact=");
|
|
476
|
+
expect(telemetry).toContain("textMisses=");
|
|
396
477
|
expect(telemetry).toContain("lookups slash(calls=");
|
|
397
478
|
expect(telemetry).toContain("exports=");
|
|
398
479
|
expect(telemetry).toContain("pins=");
|
|
399
480
|
});
|
|
400
481
|
|
|
401
|
-
it("
|
|
482
|
+
it("keeps current context_lookup results visible before the model consumes them", async () => {
|
|
402
483
|
const { pi, handlers, commands, tools } = createPiMock();
|
|
403
|
-
registerContextBrokerBeta(pi, { lookupBytes:
|
|
484
|
+
registerContextBrokerBeta(pi, { lookupBytes: 1000, rewriteThresholdBytes: 1 });
|
|
485
|
+
const { ctx } = createCtx();
|
|
486
|
+
|
|
487
|
+
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
488
|
+
await runHandlers(handlers, "tool_result", {
|
|
489
|
+
type: "tool_result",
|
|
490
|
+
toolCallId: "call-current-lookup-source",
|
|
491
|
+
toolName: "bash",
|
|
492
|
+
input: { command: "printf current-lookup" },
|
|
493
|
+
content: [{ type: "text", text: "CURRENT_LOOKUP_EVIDENCE_" + "x".repeat(120) }],
|
|
494
|
+
isError: false,
|
|
495
|
+
}, ctx);
|
|
496
|
+
const handle = commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
497
|
+
const lookupResult = await tools.get("context_lookup").execute("lookup-current", { handle }, undefined, undefined, ctx);
|
|
498
|
+
|
|
499
|
+
const contextResult = await handlers.get("context")?.[0]({
|
|
500
|
+
type: "context",
|
|
501
|
+
messages: [{ role: "toolResult", toolCallId: "lookup-current", toolName: "context_lookup", content: lookupResult.content, isError: false }],
|
|
502
|
+
}, ctx);
|
|
503
|
+
|
|
504
|
+
expect(contextResult).toBeUndefined();
|
|
505
|
+
expect(lookupResult.content[0].text).toContain("CURRENT_LOOKUP_EVIDENCE_");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("does not broker historical context_lookup results recursively", async () => {
|
|
509
|
+
const { pi, handlers, commands, tools } = createPiMock();
|
|
510
|
+
registerContextBrokerBeta(pi, { lookupBytes: 500 });
|
|
404
511
|
const { ctx, notifications } = createCtx();
|
|
405
512
|
|
|
406
513
|
await runHandlers(handlers, "session_start", { type: "session_start" }, ctx);
|
|
@@ -425,7 +532,10 @@ lookupBytes: 500,
|
|
|
425
532
|
}, ctx);
|
|
426
533
|
const contextResult = await handlers.get("context")?.[0]({
|
|
427
534
|
type: "context",
|
|
428
|
-
messages: [
|
|
535
|
+
messages: [
|
|
536
|
+
{ role: "toolResult", toolCallId: "lookup-call", toolName: "context_lookup", content: lookupResult.content, isError: false },
|
|
537
|
+
{ role: "assistant", content: [{ type: "text", text: "I consumed the lookup." }] },
|
|
538
|
+
],
|
|
429
539
|
}, ctx);
|
|
430
540
|
await commands.get("context").handler("brief", ctx);
|
|
431
541
|
|
|
@@ -702,6 +812,7 @@ maxRecords: 1,
|
|
|
702
812
|
const secondHandle = second.commands.get("context").getArgumentCompletions("lookup ")?.[0].value.replace(/^lookup /, "");
|
|
703
813
|
await second.commands.get("context").handler(`lookup ${handle}`, secondRun.ctx);
|
|
704
814
|
await second.commands.get("context").handler("brief", secondRun.ctx);
|
|
815
|
+
await second.commands.get("context").handler("status", secondRun.ctx);
|
|
705
816
|
|
|
706
817
|
const third = createPiMock();
|
|
707
818
|
const thirdRun = createCtx();
|
|
@@ -710,9 +821,10 @@ maxRecords: 1,
|
|
|
710
821
|
await third.commands.get("context").handler(`lookup ${secondHandle}`, thirdRun.ctx);
|
|
711
822
|
await third.commands.get("context").handler("brief", thirdRun.ctx);
|
|
712
823
|
|
|
713
|
-
expect(secondRun.notifications.at(-
|
|
714
|
-
expect(secondRun.notifications.at(-
|
|
715
|
-
expect(secondRun.notifications.at(-
|
|
824
|
+
expect(secondRun.notifications.at(-4)?.message).toContain("durable payload");
|
|
825
|
+
expect(secondRun.notifications.at(-3)?.message).toContain("tier=hot");
|
|
826
|
+
expect(secondRun.notifications.at(-3)?.message).toContain("pinned");
|
|
827
|
+
expect(secondRun.notifications.at(-2)?.message).toContain("globalCaps=records:2048 bytes:268435456");
|
|
716
828
|
expect(thirdRun.notifications.at(-2)?.message).toContain("durable payload");
|
|
717
829
|
expect(thirdRun.notifications.at(-1)?.message).toContain("tier=hot");
|
|
718
830
|
expect(thirdRun.notifications.at(-1)?.message).toContain("pinned");
|
|
@@ -20,6 +20,8 @@ export interface ContextBrokerBetaOptions {
|
|
|
20
20
|
lookupBytes?: number;
|
|
21
21
|
searchBytes?: number;
|
|
22
22
|
rewriteThresholdBytes?: number;
|
|
23
|
+
hotToWarmMs?: number;
|
|
24
|
+
warmToColdMs?: number;
|
|
23
25
|
durable?: boolean;
|
|
24
26
|
storeDir?: string;
|
|
25
27
|
}
|
|
@@ -32,6 +34,10 @@ const DEFAULT_LOOKUP_BYTES = 12_000;
|
|
|
32
34
|
const DEFAULT_SEARCH_BYTES = 2_000;
|
|
33
35
|
const DEFAULT_REWRITE_THRESHOLD_BYTES = 8 * 1024;
|
|
34
36
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
37
|
+
const DEFAULT_HOT_TO_WARM_MS = 2 * 60 * 60 * 1000;
|
|
38
|
+
const DEFAULT_WARM_TO_COLD_MS = 12 * 60 * 60 * 1000;
|
|
39
|
+
const DEFAULT_DURABLE_GLOBAL_MAX_RECORDS = 2_048;
|
|
40
|
+
const DEFAULT_DURABLE_GLOBAL_MAX_BYTES = 256 * 1024 * 1024;
|
|
35
41
|
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
36
42
|
|
|
37
43
|
function envFlag(name: string): boolean {
|
|
@@ -89,6 +95,28 @@ function isHostilePayload(payload: string): boolean {
|
|
|
89
95
|
return hasHostileText(payload);
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
function isOpaquePayload(payload: string): boolean {
|
|
99
|
+
return hasOpaqueText(payload);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasOpaqueText(text: string): boolean {
|
|
103
|
+
const value = String(text ?? "");
|
|
104
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
105
|
+
if (bytes < 4096) return false;
|
|
106
|
+
|
|
107
|
+
const compacted = value.replace(/\s+/g, "");
|
|
108
|
+
if (compacted.length >= 4096 && /^[A-Za-z0-9+/=_-]+$/.test(compacted) && compacted.length / Math.max(1, value.length) > 0.85) return true;
|
|
109
|
+
if (/\b(?:[A-Fa-f0-9]{2}){2048,}\b/.test(value)) return true;
|
|
110
|
+
|
|
111
|
+
const lines = value.split(/\r?\n/);
|
|
112
|
+
const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
113
|
+
const whitespace = (value.match(/\s/g) ?? []).length;
|
|
114
|
+
const whitespaceRatio = whitespace / Math.max(1, value.length);
|
|
115
|
+
if (longestLine >= 4096 && whitespaceRatio < 0.03) return true;
|
|
116
|
+
if (longestLine >= 2048 && whitespaceRatio < 0.02 && /[{};:,]/.test(value)) return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
function hasHostileText(text: string): boolean {
|
|
93
121
|
let suspicious = 0;
|
|
94
122
|
let scanned = 0;
|
|
@@ -117,12 +145,24 @@ function hasHostileValue(value: unknown): boolean {
|
|
|
117
145
|
return false;
|
|
118
146
|
}
|
|
119
147
|
|
|
148
|
+
function hasOpaqueValue(value: unknown): boolean {
|
|
149
|
+
if (typeof value === "string") return hasOpaqueText(value);
|
|
150
|
+
if (Array.isArray(value)) return value.some(hasOpaqueValue);
|
|
151
|
+
if (value && typeof value === "object") {
|
|
152
|
+
return Object.values(value as Record<string, unknown>).some((entry) => hasOpaqueValue(entry));
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
120
157
|
function renderLookupOutput(item: ContextArtifact, payloadLimit: number): string {
|
|
121
158
|
const isBinary = item.tags.includes("hostile") || item.tags.includes("binary");
|
|
122
|
-
const
|
|
159
|
+
const isOpaque = item.tags.includes("opaque");
|
|
160
|
+
const payloadLines = isBinary || isOpaque
|
|
123
161
|
? [
|
|
124
162
|
"payload:",
|
|
125
|
-
|
|
163
|
+
isOpaque && !isBinary
|
|
164
|
+
? "[payload omitted from prompt because it appears opaque/high-token; use /context export"
|
|
165
|
+
: "[payload intentionally omitted from prompt for safety; use /context export",
|
|
126
166
|
sanitizeForPrompt(item.handle),
|
|
127
167
|
"for full content]",
|
|
128
168
|
]
|
|
@@ -176,6 +216,26 @@ function compact(value: string, max = 120): string {
|
|
|
176
216
|
return truncateUtf8(value.replace(/\s+/g, " ").trim(), max);
|
|
177
217
|
}
|
|
178
218
|
|
|
219
|
+
function capText(value: number): string {
|
|
220
|
+
return Number.isFinite(value) ? String(value) : "unbounded";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function lookupMissMessage(exact: boolean): string {
|
|
224
|
+
return exact
|
|
225
|
+
? "No context artifact matched that exact handle. The artifact may be missing, expired, pruned, or from a non-durable prior session."
|
|
226
|
+
: "No context artifacts matched that text/filter query. Try an exact ctx:// handle, narrower path/tag/kind/tier filters, or a more specific search term.";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function utf8Bytes(text: string): number {
|
|
230
|
+
return Buffer.byteLength(text, "utf8");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function promptPayloadBytes(message: any): number {
|
|
234
|
+
if (message?.role === "bashExecution") return utf8Bytes(String(message.output ?? ""));
|
|
235
|
+
if (message?.role === "toolResult") return utf8Bytes(contentText(message.content));
|
|
236
|
+
return utf8Bytes(toText(message));
|
|
237
|
+
}
|
|
238
|
+
|
|
179
239
|
function stableHash(value: string): string {
|
|
180
240
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
181
241
|
}
|
|
@@ -283,14 +343,16 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
283
343
|
options.rewriteThresholdBytes
|
|
284
344
|
?? envNonNegativeInt("PI_CONTEXT_BROKER_REWRITE_THRESHOLD_BYTES")
|
|
285
345
|
?? DEFAULT_REWRITE_THRESHOLD_BYTES;
|
|
346
|
+
const durable = options.durable ?? (envFlag("PI_CONTEXT_BROKER_DURABLE") || Boolean(options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR));
|
|
286
347
|
const brokerOptions = {
|
|
287
348
|
maxRecords: options.maxRecords ?? 64,
|
|
288
349
|
maxBytes: options.maxBytes ?? 8 * 1024 * 1024,
|
|
289
|
-
globalMaxRecords: options.globalMaxRecords ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS"),
|
|
290
|
-
globalMaxBytes: options.globalMaxBytes ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES"),
|
|
350
|
+
globalMaxRecords: options.globalMaxRecords ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_RECORDS") ?? (durable ? DEFAULT_DURABLE_GLOBAL_MAX_RECORDS : undefined),
|
|
351
|
+
globalMaxBytes: options.globalMaxBytes ?? envNonNegativeInt("PI_CONTEXT_BROKER_GLOBAL_MAX_BYTES") ?? (durable ? DEFAULT_DURABLE_GLOBAL_MAX_BYTES : undefined),
|
|
352
|
+
hotToWarmMs: options.hotToWarmMs ?? envNonNegativeInt("PI_CONTEXT_BROKER_HOT_TO_WARM_MS") ?? DEFAULT_HOT_TO_WARM_MS,
|
|
353
|
+
warmToColdMs: options.warmToColdMs ?? envNonNegativeInt("PI_CONTEXT_BROKER_WARM_TO_COLD_MS") ?? DEFAULT_WARM_TO_COLD_MS,
|
|
291
354
|
briefBytes,
|
|
292
355
|
};
|
|
293
|
-
const durable = options.durable ?? (envFlag("PI_CONTEXT_BROKER_DURABLE") || Boolean(options.storeDir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR));
|
|
294
356
|
const durableBackend = String(process.env.PI_CONTEXT_BROKER_BACKEND ?? "sqlite").trim().toLowerCase();
|
|
295
357
|
const broker = durable
|
|
296
358
|
? durableBackend === "jsonl"
|
|
@@ -308,6 +370,9 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
308
370
|
contextHookBash: 0,
|
|
309
371
|
contextHookBashRewrites: 0,
|
|
310
372
|
contextHookBashHostile: 0,
|
|
373
|
+
contextHookRewriteRawBytes: 0,
|
|
374
|
+
contextHookRewriteReplacementBytes: 0,
|
|
375
|
+
contextHookContextLookupHistoryOmissions: 0,
|
|
311
376
|
toolResultEvents: 0,
|
|
312
377
|
toolResultArtifacts: 0,
|
|
313
378
|
backfillScans: 0,
|
|
@@ -318,24 +383,38 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
318
383
|
toolLookupTextCalls: 0,
|
|
319
384
|
toolLookupHits: 0,
|
|
320
385
|
toolLookupMisses: 0,
|
|
386
|
+
toolLookupExactMisses: 0,
|
|
387
|
+
toolLookupTextMisses: 0,
|
|
321
388
|
commandLookupCalls: 0,
|
|
322
389
|
commandLookupExactCalls: 0,
|
|
323
390
|
commandLookupTextCalls: 0,
|
|
324
391
|
commandLookupHits: 0,
|
|
325
392
|
commandLookupMisses: 0,
|
|
393
|
+
commandLookupExactMisses: 0,
|
|
394
|
+
commandLookupTextMisses: 0,
|
|
326
395
|
exportCalls: 0,
|
|
327
396
|
pinCalls: 0,
|
|
328
397
|
statusCalls: 0,
|
|
329
398
|
pruneCalls: 0,
|
|
330
399
|
};
|
|
331
400
|
|
|
401
|
+
function recordContextRewrite(rawBytes: number, replacementBytes: number): void {
|
|
402
|
+
routingTelemetry.contextHookRewriteRawBytes += Math.max(0, rawBytes);
|
|
403
|
+
routingTelemetry.contextHookRewriteReplacementBytes += Math.max(0, replacementBytes);
|
|
404
|
+
}
|
|
405
|
+
|
|
332
406
|
function formatRoutingTelemetry(): string {
|
|
407
|
+
const savedBytes = Math.max(0, routingTelemetry.contextHookRewriteRawBytes - routingTelemetry.contextHookRewriteReplacementBytes);
|
|
408
|
+
const savedPct = routingTelemetry.contextHookRewriteRawBytes > 0
|
|
409
|
+
? ((savedBytes / routingTelemetry.contextHookRewriteRawBytes) * 100).toFixed(1)
|
|
410
|
+
: "0.0";
|
|
333
411
|
const line = [
|
|
334
412
|
`contextHook calls=${routingTelemetry.contextHookCalls}`,
|
|
335
413
|
`toolResults seen=${routingTelemetry.contextHookToolResults} rewritten=${routingTelemetry.contextHookToolResultRewrites} hostile=${routingTelemetry.contextHookToolResultHostile}`,
|
|
336
414
|
`bash seen=${routingTelemetry.contextHookBash} rewritten=${routingTelemetry.contextHookBashRewrites} hostile=${routingTelemetry.contextHookBashHostile}`,
|
|
337
|
-
`
|
|
338
|
-
`lookups
|
|
415
|
+
`rewriteSavings rawBytes=${routingTelemetry.contextHookRewriteRawBytes} replacementBytes=${routingTelemetry.contextHookRewriteReplacementBytes} savedBytes=${savedBytes} savedPct=${savedPct}% contextLookupHistoryOmitted=${routingTelemetry.contextHookContextLookupHistoryOmissions}`,
|
|
416
|
+
`lookups tool(calls=${routingTelemetry.toolLookupCalls}, exact=${routingTelemetry.toolLookupExactCalls}, text=${routingTelemetry.toolLookupTextCalls}, hits=${routingTelemetry.toolLookupHits}, misses=${routingTelemetry.toolLookupMisses}, exactMisses=${routingTelemetry.toolLookupExactMisses}, textMisses=${routingTelemetry.toolLookupTextMisses})`,
|
|
417
|
+
`lookups slash(calls=${routingTelemetry.commandLookupCalls}, exact=${routingTelemetry.commandLookupExactCalls}, text=${routingTelemetry.commandLookupTextCalls}, hits=${routingTelemetry.commandLookupHits}, misses=${routingTelemetry.commandLookupMisses}, exactMisses=${routingTelemetry.commandLookupExactMisses}, textMisses=${routingTelemetry.commandLookupTextMisses})`,
|
|
339
418
|
`exports=${routingTelemetry.exportCalls}`,
|
|
340
419
|
`pins=${routingTelemetry.pinCalls}`,
|
|
341
420
|
`pruneCalls=${routingTelemetry.pruneCalls}`,
|
|
@@ -377,6 +456,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
377
456
|
const payload = toolPayload(sanitizedEvent);
|
|
378
457
|
const bytes = Buffer.byteLength(payload, "utf8");
|
|
379
458
|
const hostilePayload = isHostilePayload(payload) || hasHostileValue(sanitizedEvent);
|
|
459
|
+
const opaquePayload = !hostilePayload && (isOpaquePayload(payload) || hasOpaqueValue(sanitizedEvent));
|
|
380
460
|
const artifact = broker.publish({
|
|
381
461
|
sessionId: activeSessionId,
|
|
382
462
|
kind: "tool_output",
|
|
@@ -387,6 +467,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
387
467
|
event.isError ? "error" : "ok",
|
|
388
468
|
event.sourceId ? "session-backfill" : "live",
|
|
389
469
|
...(hostilePayload ? ["hostile", "binary"] : []),
|
|
470
|
+
...(opaquePayload ? ["opaque"] : []),
|
|
390
471
|
],
|
|
391
472
|
command: event.toolName === "bash" && typeof sanitizedEvent.input?.command === "string" ? sanitizedEvent.input.command : undefined,
|
|
392
473
|
paths: typeof sanitizedEvent.input?.path === "string" ? [sanitizedEvent.input.path] : [],
|
|
@@ -572,19 +653,35 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
572
653
|
activeSessionId = sessionIdFor(ctx);
|
|
573
654
|
routingTelemetry.contextHookCalls += 1;
|
|
574
655
|
const toolInputs = collectToolInputs(event.messages);
|
|
575
|
-
|
|
656
|
+
type RewriteDraft = {
|
|
657
|
+
original: any;
|
|
658
|
+
replacement?: any;
|
|
659
|
+
rawBytes?: number;
|
|
660
|
+
artifact?: ContextArtifact;
|
|
661
|
+
rewrite?: (artifact: ContextArtifact) => any;
|
|
662
|
+
safeFallback?: any;
|
|
663
|
+
};
|
|
664
|
+
const drafts = event.messages.map((message: any, index: number): RewriteDraft => {
|
|
576
665
|
if (message?.role === "toolResult") {
|
|
577
666
|
routingTelemetry.contextHookToolResults += 1;
|
|
578
667
|
const raw = contentText(message.content);
|
|
668
|
+
const rawBytes = utf8Bytes(raw);
|
|
579
669
|
const toolInput = typeof message.toolCallId === "string" ? toolInputs.get(message.toolCallId) : undefined;
|
|
580
670
|
const toolName = String(message.toolName ?? toolInput?.toolName ?? "tool");
|
|
581
671
|
const hostile = hasHostileText(raw) || hasHostileValue(message.content);
|
|
582
672
|
if (hostile) routingTelemetry.contextHookToolResultHostile += 1;
|
|
583
|
-
const shouldRewrite = Buffer.byteLength(raw, "utf8") > rewriteThresholdBytes || hostile;
|
|
584
|
-
if (!shouldRewrite) return { original: message };
|
|
585
673
|
if (!shouldBrokerToolName(toolName)) {
|
|
586
|
-
|
|
674
|
+
const hasLaterAssistant = event.messages.slice(index + 1).some((candidate: any) => candidate?.role === "assistant");
|
|
675
|
+
if (!hasLaterAssistant) return { original: message };
|
|
676
|
+
routingTelemetry.contextHookContextLookupHistoryOmissions += 1;
|
|
677
|
+
return {
|
|
678
|
+
original: message,
|
|
679
|
+
rawBytes,
|
|
680
|
+
replacement: { ...message, content: [{ type: "text", text: contextLookupHistoryPlaceholder() }] },
|
|
681
|
+
};
|
|
587
682
|
}
|
|
683
|
+
const shouldRewrite = rawBytes > rewriteThresholdBytes || hostile;
|
|
684
|
+
if (!shouldRewrite) return { original: message };
|
|
588
685
|
const artifact = publishToolArtifact({
|
|
589
686
|
toolName,
|
|
590
687
|
input: message.input ?? toolInput?.input,
|
|
@@ -599,6 +696,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
599
696
|
routingTelemetry.contextHookToolResultRewrites += 1;
|
|
600
697
|
return {
|
|
601
698
|
original: message,
|
|
699
|
+
rawBytes,
|
|
602
700
|
artifact,
|
|
603
701
|
rewrite: (live) => ({ ...message, content: [{ type: "text", text: brokerPlaceholder(live) }] }),
|
|
604
702
|
safeFallback: { ...message, content: [{ type: "text", text: prunedPayloadPlaceholder(hostile) }] },
|
|
@@ -608,9 +706,10 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
608
706
|
if (message?.role === "bashExecution" && message.excludeFromContext !== true) {
|
|
609
707
|
routingTelemetry.contextHookBash += 1;
|
|
610
708
|
const raw = String(message.output ?? "");
|
|
709
|
+
const rawBytes = utf8Bytes(raw);
|
|
611
710
|
const hostile = hasHostileText(raw) || hasHostileValue(message.output);
|
|
612
711
|
if (hostile) routingTelemetry.contextHookBashHostile += 1;
|
|
613
|
-
const shouldRewrite =
|
|
712
|
+
const shouldRewrite = rawBytes > rewriteThresholdBytes || hostile;
|
|
614
713
|
if (!shouldRewrite) return { original: message };
|
|
615
714
|
const sourceId = typeof message.timestamp === "number"
|
|
616
715
|
? `bash:${message.timestamp}:${stableHash([message.command ?? "", raw, message.exitCode ?? "", message.cancelled ?? ""].join("\n"))}`
|
|
@@ -634,6 +733,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
634
733
|
routingTelemetry.contextHookBashRewrites += 1;
|
|
635
734
|
return {
|
|
636
735
|
original: message,
|
|
736
|
+
rawBytes,
|
|
637
737
|
artifact,
|
|
638
738
|
rewrite: (live) => ({ ...message, output: brokerPlaceholder(live), truncated: true }),
|
|
639
739
|
safeFallback: { ...message, output: prunedPayloadPlaceholder(hostile), truncated: true },
|
|
@@ -647,6 +747,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
647
747
|
const messages = drafts.map((draft) => {
|
|
648
748
|
if (draft.replacement) {
|
|
649
749
|
changed = true;
|
|
750
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(draft.replacement));
|
|
650
751
|
return draft.replacement;
|
|
651
752
|
}
|
|
652
753
|
if (!draft.artifact || !draft.rewrite) return draft.original;
|
|
@@ -655,12 +756,15 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
655
756
|
for (const parentId of draft.artifact.parentIds) sourceHandles.delete(parentId);
|
|
656
757
|
if (draft.safeFallback) {
|
|
657
758
|
changed = true;
|
|
759
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(draft.safeFallback));
|
|
658
760
|
return draft.safeFallback;
|
|
659
761
|
}
|
|
660
762
|
return draft.original;
|
|
661
763
|
}
|
|
662
764
|
changed = true;
|
|
663
|
-
|
|
765
|
+
const replacement = draft.rewrite(live);
|
|
766
|
+
recordContextRewrite(draft.rawBytes ?? promptPayloadBytes(draft.original), promptPayloadBytes(replacement));
|
|
767
|
+
return replacement;
|
|
664
768
|
});
|
|
665
769
|
|
|
666
770
|
return changed ? { messages } : undefined;
|
|
@@ -719,7 +823,9 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
719
823
|
});
|
|
720
824
|
if (!results.length) {
|
|
721
825
|
routingTelemetry.toolLookupMisses += 1;
|
|
722
|
-
|
|
826
|
+
if (exact) routingTelemetry.toolLookupExactMisses += 1;
|
|
827
|
+
else routingTelemetry.toolLookupTextMisses += 1;
|
|
828
|
+
return textResult(lookupMissMessage(exact));
|
|
723
829
|
}
|
|
724
830
|
routingTelemetry.toolLookupHits += 1;
|
|
725
831
|
return textResult(results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n"));
|
|
@@ -738,7 +844,7 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
738
844
|
routingTelemetry.statusCalls += 1;
|
|
739
845
|
const status = broker.status();
|
|
740
846
|
ctx.ui.notify(
|
|
741
|
-
`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`,
|
|
847
|
+
`Context broker: enabled, session=${activeSessionId}, records=${status.records}/${status.maxRecords}, bytes=${status.bytes}/${status.maxBytes}, globalCaps=records:${capText(status.globalMaxRecords)} bytes:${capText(status.globalMaxBytes)}, tiers=hot:${status.hotRecords}/${status.hotBytes} warm:${status.warmRecords}/${status.warmBytes} cold:${status.coldRecords}/${status.coldBytes}, pinned=${status.pinnedRecords}/${status.pinnedBytes} bytes`,
|
|
742
848
|
"info",
|
|
743
849
|
);
|
|
744
850
|
ctx.ui.notify(formatRoutingTelemetry(), "info");
|
|
@@ -764,8 +870,10 @@ export async function registerContextBrokerBeta(pi: ExtensionAPI, options: Conte
|
|
|
764
870
|
routingTelemetry.commandLookupHits += 1;
|
|
765
871
|
} else {
|
|
766
872
|
routingTelemetry.commandLookupMisses += 1;
|
|
873
|
+
if (exact) routingTelemetry.commandLookupExactMisses += 1;
|
|
874
|
+
else routingTelemetry.commandLookupTextMisses += 1;
|
|
767
875
|
}
|
|
768
|
-
ctx.ui.notify(results.length ? results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n") :
|
|
876
|
+
ctx.ui.notify(results.length ? results.map((item) => renderLookupOutput(item, exact ? lookupBytes : searchBytes)).join("\n\n---\n\n") : lookupMissMessage(exact), "info");
|
|
769
877
|
return;
|
|
770
878
|
}
|
|
771
879
|
|
|
@@ -193,6 +193,38 @@ describe("createInMemoryContextBroker", () => {
|
|
|
193
193
|
expect(broker.lookup({ sessionId: "s", tier: "cold" }).map((artifact) => artifact.id)).toEqual([explicit.id, archive.id]);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it("cools old artifacts across hot, warm, and cold tiers without deleting them", () => {
|
|
197
|
+
const broker = createInMemoryContextBroker({ briefBytes: 1200, defaultTtlMs: 0, hotToWarmMs: 100, warmToColdMs: 200 });
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const oldHot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "old-hot", summary: "old hot", tier: "hot", createdAt: now - 300 });
|
|
200
|
+
const oldWarm = broker.publish({ sessionId: "s", kind: "tool_output", payload: "old-warm", summary: "old warm", tier: "warm", createdAt: now - 300 });
|
|
201
|
+
const freshHot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "fresh-hot", summary: "fresh hot", tier: "hot", createdAt: now - 50 });
|
|
202
|
+
const pinned = broker.publish({ sessionId: "s", kind: "tool_output", payload: "pinned", summary: "pinned hot", tier: "hot", pinned: true, createdAt: now - 300 });
|
|
203
|
+
|
|
204
|
+
broker.prune(now);
|
|
205
|
+
|
|
206
|
+
expect(broker.lookup({ handle: oldHot.handle })[0]?.tier).toBe("cold");
|
|
207
|
+
expect(broker.lookup({ handle: oldWarm.handle })[0]?.tier).toBe("cold");
|
|
208
|
+
expect(broker.lookup({ handle: freshHot.handle })[0]?.tier).toBe("hot");
|
|
209
|
+
expect(broker.lookup({ handle: pinned.handle })[0]?.tier).toBe("hot");
|
|
210
|
+
const brief = broker.renderBrief({ sessionId: "s" });
|
|
211
|
+
expect(brief).not.toContain(oldHot.handle);
|
|
212
|
+
expect(brief).not.toContain(oldWarm.handle);
|
|
213
|
+
expect(brief).toContain(freshHot.handle);
|
|
214
|
+
expect(brief).toContain(pinned.handle);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("cools protected new artifacts before enforcing tier caps", () => {
|
|
218
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 0, maxRecords: 10, hotMaxRecords: 1, hotToWarmMs: 10_000, warmToColdMs: 20_000 });
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const fresh = broker.publish({ sessionId: "s", kind: "tool_output", payload: "fresh", summary: "fresh", tier: "hot", createdAt: now - 1_000 });
|
|
221
|
+
const aged = broker.publish({ sessionId: "s", kind: "tool_output", payload: "aged", summary: "aged", tier: "hot", createdAt: now - 30_000 });
|
|
222
|
+
|
|
223
|
+
expect(aged.tier).toBe("cold");
|
|
224
|
+
expect(broker.lookup({ handle: fresh.handle })[0]?.tier).toBe("hot");
|
|
225
|
+
expect(broker.lookup({ handle: aged.handle })[0]?.tier).toBe("cold");
|
|
226
|
+
});
|
|
227
|
+
|
|
196
228
|
it("renders prompt briefs hot-first, warm-second, and excludes cold unless explicit", () => {
|
|
197
229
|
const broker = createInMemoryContextBroker({ briefBytes: 900, defaultTtlMs: 0 });
|
|
198
230
|
const cold = broker.publish({ sessionId: "s", kind: "tool_output", payload: "cold", summary: "cold archive", tier: "cold", createdAt: 1 });
|