@fiale-plus/pi-rogue-bundle 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  It stitches together (and bundles for a true single-package install):
6
6
 
7
+ - `@fiale-plus/pi-core` (shared contracts/helpers)
7
8
  - `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
9
+ - `@fiale-plus/pi-rogue-context-broker` (beta context-broker runtime; disabled by default)
8
10
  - `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
9
11
 
10
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.
@@ -26,12 +28,17 @@ npm install
26
28
  ## Scope boundaries
27
29
 
28
30
  - **Lab / internal helpers are excluded from this bundle.**
31
+ - The beta context-broker runtime is bundled for opt-in experiments but is not registered/enabled by default.
32
+ - Opt-in consumers can import the runtime through the bundle subpath: `@fiale-plus/pi-rogue-bundle/context-broker`.
33
+ - Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi to register the beta `/context` command surface and prompt-load rewriting.
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.
29
35
  - `@fiale-plus/pi-rogue-bundle` is the only published surface for the logic.
30
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.
31
37
 
32
38
  ## Command surface
33
39
 
34
- - `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
40
+ - Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
41
+ - Opt-in beta: `PI_CONTEXT_BROKER_ENABLED=true` adds `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, and `/context prune` with autocomplete.
35
42
 
36
43
  ## Status
37
44
 
@@ -2,11 +2,12 @@
2
2
 
3
3
  Shared helpers for the Pi-Rogue workspace.
4
4
 
5
- Includes the first bounded context broker contract and in-memory implementation:
5
+ Includes shared bounded context broker contracts:
6
6
 
7
- - `createInMemoryContextBroker()` stores artifacts behind stable `ctx://...` handles.
8
- - Lookups support handle, session, kind, tag, path, command prefix, branch, and text filters.
9
- - Omitted summaries become metadata-only placeholders, keeping raw payloads out of prompt briefs by default.
10
- - Pruning enforces per-session record/byte caps, TTL expiry on reads, and pinned-artifact retention.
7
+ - `BoundedContextBroker`
8
+ - `ContextArtifact` / `ContextArtifactInput`
9
+ - lookup, retention, and status type definitions
10
+
11
+ The executable in-memory implementation lives in `@fiale-plus/pi-rogue-context-broker`.
11
12
 
12
13
  Install locally from this repo root: `npm install`
@@ -1,6 +1,3 @@
1
- import { createHash } from "node:crypto";
2
- import { safeName } from "./text.js";
3
-
4
1
  export type ContextArtifactKind =
5
2
  | "tool_output"
6
3
  | "diff"
@@ -9,6 +6,8 @@ export type ContextArtifactKind =
9
6
  | "advisor_brief"
10
7
  | "memory_note";
11
8
 
9
+ export type ContextArtifactTier = "hot" | "warm" | "cold";
10
+
12
11
  export interface ContextArtifactInput {
13
12
  sessionId: string;
14
13
  kind: ContextArtifactKind;
@@ -18,6 +17,7 @@ export interface ContextArtifactInput {
18
17
  paths?: string[];
19
18
  command?: string;
20
19
  branch?: string;
20
+ tier?: ContextArtifactTier;
21
21
  ttlMs?: number;
22
22
  pinned?: boolean;
23
23
  parentIds?: string[];
@@ -39,6 +39,7 @@ export interface ContextArtifact {
39
39
  paths: string[];
40
40
  command?: string;
41
41
  branch?: string;
42
+ tier: ContextArtifactTier;
42
43
  expiresAt?: number;
43
44
  pinned: boolean;
44
45
  parentIds: string[];
@@ -53,6 +54,7 @@ export interface ContextLookupQuery {
53
54
  path?: string;
54
55
  commandPrefix?: string;
55
56
  branch?: string;
57
+ tier?: ContextArtifactTier;
56
58
  text?: string;
57
59
  limit?: number;
58
60
  }
@@ -62,6 +64,12 @@ export interface ContextBrokerStatus {
62
64
  bytes: number;
63
65
  pinnedRecords: number;
64
66
  pinnedBytes: number;
67
+ hotRecords: number;
68
+ hotBytes: number;
69
+ warmRecords: number;
70
+ warmBytes: number;
71
+ coldRecords: number;
72
+ coldBytes: number;
65
73
  maxRecords: number;
66
74
  maxBytes: number;
67
75
  }
@@ -70,6 +78,15 @@ export interface ContextBrokerOptions {
70
78
  maxRecords?: number;
71
79
  maxBytes?: number;
72
80
  defaultTtlMs?: number;
81
+ hotTtlMs?: number;
82
+ warmTtlMs?: number;
83
+ coldTtlMs?: number;
84
+ hotMaxRecords?: number;
85
+ warmMaxRecords?: number;
86
+ coldMaxRecords?: number;
87
+ hotMaxBytes?: number;
88
+ warmMaxBytes?: number;
89
+ coldMaxBytes?: number;
73
90
  summaryBytes?: number;
74
91
  briefBytes?: number;
75
92
  }
@@ -82,227 +99,3 @@ export interface BoundedContextBroker {
82
99
  status(): ContextBrokerStatus;
83
100
  renderBrief(query?: ContextLookupQuery & { budgetBytes?: number }): string;
84
101
  }
85
-
86
- const DEFAULT_MAX_RECORDS = 256;
87
- const DEFAULT_MAX_BYTES = 128 * 1024 * 1024;
88
- const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
89
- const DEFAULT_SUMMARY_BYTES = 320;
90
- const DEFAULT_BRIEF_BYTES = 2_000;
91
-
92
- function normalizeList(values: string[] | undefined): string[] {
93
- return [...new Set((values ?? []).map((value) => String(value || "").trim()).filter(Boolean))];
94
- }
95
-
96
- function payloadText(payload: string | Buffer): string {
97
- return Buffer.isBuffer(payload) ? payload.toString("utf8") : String(payload ?? "");
98
- }
99
-
100
- function payloadBytes(payload: string | Buffer): number {
101
- return Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(String(payload ?? ""), "utf8");
102
- }
103
-
104
- function hashPayload(payload: string | Buffer): string {
105
- return createHash("sha256").update(Buffer.isBuffer(payload) ? payload : String(payload)).digest("hex");
106
- }
107
-
108
- function normalizeNeedle(value: string | undefined): string {
109
- return String(value ?? "").trim().toLowerCase();
110
- }
111
-
112
- function truncateUtf8(text: string, maxBytes: number): string {
113
- const limit = Math.max(0, Math.floor(maxBytes));
114
- if (Buffer.byteLength(text, "utf8") <= limit) return text;
115
- if (limit === 0) return "";
116
-
117
- const ellipsis = "…";
118
- const ellipsisBytes = Buffer.byteLength(ellipsis, "utf8");
119
- const contentLimit = Math.max(0, limit - ellipsisBytes);
120
- let used = 0;
121
- let result = "";
122
-
123
- for (const char of text) {
124
- const bytes = Buffer.byteLength(char, "utf8");
125
- if (used + bytes > contentLimit) break;
126
- result += char;
127
- used += bytes;
128
- }
129
-
130
- if (Buffer.byteLength(result + ellipsis, "utf8") <= limit) return result + ellipsis;
131
- return result;
132
- }
133
-
134
- function summarizeArtifact(summary: string | undefined, kind: ContextArtifactKind, bytes: number, sha256: string, maxBytes: number): string {
135
- const cleaned = String(summary ?? "").replace(/\s+/g, " ").trim();
136
- if (cleaned) return truncateUtf8(cleaned, maxBytes);
137
- return truncateUtf8(`[${kind} payload stored externally; ${bytes} bytes; sha256=${sha256.slice(0, 16)}]`, maxBytes);
138
- }
139
-
140
- function artifactMatches(artifact: ContextArtifact, query: ContextLookupQuery): boolean {
141
- if (query.id && artifact.id !== query.id) return false;
142
- if (query.handle && artifact.handle !== query.handle) return false;
143
- if (query.sessionId && artifact.sessionId !== query.sessionId) return false;
144
- if (query.kind && artifact.kind !== query.kind) return false;
145
- if (query.branch && artifact.branch !== query.branch) return false;
146
- if (query.tag && !artifact.tags.includes(query.tag)) return false;
147
- if (query.path) {
148
- const queryPath = query.path.replace(/\/$/, "");
149
- if (!artifact.paths.some((path) => path === query.path || path.startsWith(`${queryPath}/`))) return false;
150
- }
151
- if (query.commandPrefix && !artifact.command?.startsWith(query.commandPrefix)) return false;
152
-
153
- const text = normalizeNeedle(query.text);
154
- if (text) {
155
- const haystack = [artifact.summary, artifact.payload, artifact.command, artifact.tags.join(" "), artifact.paths.join(" ")]
156
- .join("\n")
157
- .toLowerCase();
158
- if (!haystack.includes(text)) return false;
159
- }
160
-
161
- return true;
162
- }
163
-
164
- export function createInMemoryContextBroker(options: ContextBrokerOptions = {}): BoundedContextBroker {
165
- const maxRecords = Math.max(1, Math.floor(options.maxRecords ?? DEFAULT_MAX_RECORDS));
166
- const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_BYTES));
167
- const defaultTtlMs = Math.max(0, Math.floor(options.defaultTtlMs ?? DEFAULT_TTL_MS));
168
- const summaryBytes = Math.max(16, Math.floor(options.summaryBytes ?? DEFAULT_SUMMARY_BYTES));
169
- const defaultBriefBytes = Math.max(64, Math.floor(options.briefBytes ?? DEFAULT_BRIEF_BYTES));
170
- let artifacts: Array<ContextArtifact & { sequence: number }> = [];
171
- let sequence = 0;
172
-
173
- function currentStatus(): ContextBrokerStatus {
174
- const bytes = artifacts.reduce((sum, artifact) => sum + artifact.bytes, 0);
175
- const pinned = artifacts.filter((artifact) => artifact.pinned);
176
- return {
177
- records: artifacts.length,
178
- bytes,
179
- pinnedRecords: pinned.length,
180
- pinnedBytes: pinned.reduce((sum, artifact) => sum + artifact.bytes, 0),
181
- maxRecords,
182
- maxBytes,
183
- };
184
- }
185
-
186
- function dropExpired(now = Date.now(), protectedIds = new Set<string>()): void {
187
- artifacts = artifacts.filter(
188
- (artifact) => artifact.pinned || protectedIds.has(artifact.id) || !artifact.expiresAt || artifact.expiresAt > now,
189
- );
190
- }
191
-
192
- function oldestRemovable(sessionId: string, protectedIds: Set<string>): { artifact: ContextArtifact & { sequence: number }; index: number } | undefined {
193
- return artifacts
194
- .map((artifact, index) => ({ artifact, index }))
195
- .filter(({ artifact }) => artifact.sessionId === sessionId && !artifact.pinned && !protectedIds.has(artifact.id))
196
- .sort((a, b) => {
197
- if (a.artifact.createdAt !== b.artifact.createdAt) return a.artifact.createdAt - b.artifact.createdAt;
198
- return a.artifact.sequence - b.artifact.sequence;
199
- })[0];
200
- }
201
-
202
- function sessionWithinCaps(sessionId: string): boolean {
203
- const sessionArtifacts = artifacts.filter((artifact) => artifact.sessionId === sessionId);
204
- return sessionArtifacts.length <= maxRecords && sessionArtifacts.reduce((sum, artifact) => sum + artifact.bytes, 0) <= maxBytes;
205
- }
206
-
207
- function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
208
- dropExpired(now, protectedIds);
209
-
210
- for (const sessionId of new Set(artifacts.map((artifact) => artifact.sessionId))) {
211
- while (!sessionWithinCaps(sessionId)) {
212
- const candidate = oldestRemovable(sessionId, protectedIds);
213
- if (!candidate) break;
214
- artifacts.splice(candidate.index, 1);
215
- }
216
- }
217
-
218
- return currentStatus();
219
- }
220
-
221
- function status(): ContextBrokerStatus {
222
- dropExpired();
223
- return currentStatus();
224
- }
225
-
226
- function publish(input: ContextArtifactInput): ContextArtifact {
227
- const now = input.createdAt ?? Date.now();
228
- const payload = payloadText(input.payload);
229
- const sha256 = hashPayload(input.payload);
230
- const bytes = payloadBytes(input.payload);
231
- const artifactSequence = ++sequence;
232
- const id = `ctx-${now.toString(36)}-${String(artifactSequence).padStart(4, "0")}-${sha256.slice(0, 12)}`;
233
- const session = safeName(input.sessionId || "session");
234
- const kind = input.kind;
235
- const handle = `ctx://session/${session}/${kind}/${sha256.slice(0, 16)}/${id}`;
236
- const ttlMs = input.ttlMs ?? defaultTtlMs;
237
-
238
- const artifact: ContextArtifact & { sequence: number } = {
239
- id,
240
- handle,
241
- sessionId: input.sessionId,
242
- kind,
243
- createdAt: now,
244
- updatedAt: now,
245
- bytes,
246
- sha256,
247
- payload,
248
- summary: summarizeArtifact(input.summary, kind, bytes, sha256, summaryBytes),
249
- tags: normalizeList(input.tags),
250
- paths: normalizeList(input.paths),
251
- command: input.command?.trim() || undefined,
252
- branch: input.branch?.trim() || undefined,
253
- expiresAt: ttlMs > 0 ? now + ttlMs : undefined,
254
- pinned: Boolean(input.pinned),
255
- parentIds: normalizeList(input.parentIds),
256
- sequence: artifactSequence,
257
- };
258
-
259
- artifacts = [artifact, ...artifacts];
260
- prune(now, new Set([artifact.id]));
261
- return artifact;
262
- }
263
-
264
- function lookup(query: ContextLookupQuery = {}): ContextArtifact[] {
265
- dropExpired();
266
- const limit = Math.max(1, Math.floor(query.limit ?? (artifacts.length || 1)));
267
- return artifacts
268
- .filter((artifact) => artifactMatches(artifact, query))
269
- .sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.createdAt - a.createdAt || b.sequence - a.sequence)
270
- .slice(0, limit);
271
- }
272
-
273
- function pin(idOrHandle: string, pinned = true): ContextArtifact | null {
274
- dropExpired();
275
- const artifact = artifacts.find((candidate) => candidate.id === idOrHandle || candidate.handle === idOrHandle) ?? null;
276
- if (!artifact) return null;
277
- artifact.pinned = pinned;
278
- artifact.updatedAt = Date.now();
279
- prune();
280
- return artifacts.find((candidate) => candidate.id === artifact.id) ?? null;
281
- }
282
-
283
- function renderBrief(query: ContextLookupQuery & { budgetBytes?: number } = {}): string {
284
- const budget = Math.max(64, Math.floor(query.budgetBytes ?? defaultBriefBytes));
285
- const lines = [
286
- "## Context Broker",
287
- `Budget: ${budget} bytes`,
288
- ...lookup({ ...query, limit: query.limit ?? 8 }).map((artifact) => {
289
- const pin = artifact.pinned ? " pinned" : "";
290
- const path = artifact.paths.length ? ` paths=${artifact.paths.slice(0, 3).join(",")}` : "";
291
- const tags = artifact.tags.length ? ` tags=${artifact.tags.slice(0, 3).join(",")}` : "";
292
- return `- ${artifact.handle} kind=${artifact.kind}${pin}${path}${tags} summary="${artifact.summary}"`;
293
- }),
294
- "Lookup: use broker lookup by handle/path/tag/kind/session before replaying raw payloads.",
295
- ];
296
-
297
- return truncateUtf8(lines.join("\n"), budget);
298
- }
299
-
300
- return {
301
- publish,
302
- lookup,
303
- pin,
304
- prune,
305
- status,
306
- renderBrief,
307
- };
308
- }
@@ -283,6 +283,15 @@ function brief(s: SessionState): string {
283
283
  return lines.join("\n").slice(0, 1200);
284
284
  }
285
285
 
286
+ function contextBrokerBrief(pi: ExtensionAPI): string {
287
+ try {
288
+ const text = (pi as any).__piRogueContextBroker?.renderBrief?.();
289
+ return typeof text === "string" && text.includes("ctx://") ? sanitizeAdvisorText(text).slice(0, 2400) : "";
290
+ } catch {
291
+ return "";
292
+ }
293
+ }
294
+
286
295
  const CLIPBOARD_IMAGE_PATH_RE = /(?:\/(?:private\/)?var\/folders\/[^\s"'`<>]+\/T|\/(?:tmp|var\/tmp))\/clipboard-\d{4}-\d{2}-\d{2}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)\b/g;
287
296
 
288
297
  export function sanitizeAdvisorText(text: unknown): string {
@@ -881,12 +890,18 @@ async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: s
881
890
  const state = loadState();
882
891
  if (!question.trim()) return { text: "Ask a question.", error: "empty" };
883
892
 
884
- const ck = hash("adv", config.model ?? "auto", squish(question, 300), includeWork ? brief(state) : "");
893
+ const brokerBrief = includeWork ? contextBrokerBrief(pi) : "";
894
+ const ck = hash("adv", config.model ?? "auto", squish(question, 300), includeWork ? brief(state) : "", brokerBrief);
885
895
  const cache = loadCache();
886
896
  if (cache[ck]) { state.cacheHits++; saveState(state); return { text: cache[ck], cached: true }; }
887
897
 
888
898
  const msgs = [
889
- { role: "user", content: [ `Question: ${question}`, scope ? `Scope: ${scope}` : "", includeWork && brief(state) ? `Session:\n${brief(state)}` : "" ].filter(Boolean).join("\n"), timestamp: new Date().toISOString() },
899
+ { role: "user", content: [
900
+ `Question: ${question}`,
901
+ scope ? `Scope: ${scope}` : "",
902
+ includeWork && brief(state) ? `Session:\n${brief(state)}` : "",
903
+ brokerBrief ? `Context broker brief:\n${brokerBrief}` : "",
904
+ ].filter(Boolean).join("\n"), timestamp: new Date().toISOString() },
890
905
  ] as any[];
891
906
 
892
907
  const completed = await completeWithModelFallback(ctx, config, ADVISOR_SYSTEM, msgs, { maxTokens: 600, reasoning: "medium" as ThinkingLevel });
@@ -984,7 +999,8 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
984
999
  }
985
1000
 
986
1001
  const b = brief(state);
987
- if (!b) {
1002
+ const brokerBrief = contextBrokerBrief(pi);
1003
+ if (!b && !brokerBrief) {
988
1004
  finalDecision = "defer";
989
1005
  finalReason = "missing brief context";
990
1006
  markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
@@ -993,7 +1009,7 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
993
1009
  return;
994
1010
  }
995
1011
 
996
- const rk = hash("rev", trigger, b, delta, String(meta.fileChanged), String(meta.failed), String(meta.isAgentEnd), String(reviewRoute.label), signature);
1012
+ const rk = hash("rev", trigger, b, brokerBrief, delta, String(meta.fileChanged), String(meta.failed), String(meta.isAgentEnd), String(reviewRoute.label), signature);
997
1013
  const cache = loadCache();
998
1014
  if (cache[rk]) {
999
1015
  finalDecision = "defer";
@@ -1011,7 +1027,8 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
1011
1027
  `Delta: ${delta || "(none)"}`,
1012
1028
  `Files: ${meta.fileChanged} Errors: ${meta.failed}`,
1013
1029
  `Route: ${summarizeRoute(reviewRoute)}`,
1014
- `Brief:\n${b}`,
1030
+ b ? `Brief:\n${b}` : "",
1031
+ brokerBrief ? `Context broker brief:\n${brokerBrief}` : "",
1015
1032
  ].join("\n"), timestamp: new Date().toISOString() },
1016
1033
  ] as any[];
1017
1034
  const completed = await completeWithModelFallback(ctx, config, REVIEW_SYSTEM, msgs, { maxTokens: 400, reasoning: "low" as ThinkingLevel });
@@ -1140,13 +1157,14 @@ export function registerAdvisor(pi: ExtensionAPI): void {
1140
1157
  const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
1141
1158
  if (prompt) state.lastTask = prompt;
1142
1159
  const briefText = brief(state);
1160
+ const brokerBrief = contextBrokerBrief(pi);
1143
1161
  const intent = prompt ? classifyIntent(prompt) : "";
1144
1162
  const mode = prompt ? classifyMode(prompt) : "";
1145
1163
  const intentTag = intent ? `Intent: ${intent}` : "";
1146
1164
  const modeTag = mode ? `Mode: ${mode}` : "";
1147
1165
  // Enrich preflight text with session context so the binary gate has more signal
1148
- const enrichedText = [prompt, event.systemPrompt || "", briefText ? `Brief: ${briefText}` : "", intentTag, modeTag].filter(Boolean).join(" ");
1149
- const routeInput: AdvisorRouteInput = { phase: "preflight", text: enrichedText || prompt || event.systemPrompt || briefText || intentTag || modeTag || "", brief: briefText };
1166
+ const enrichedText = [prompt, event.systemPrompt || "", briefText ? `Brief: ${briefText}` : "", brokerBrief ? `Context broker: ${brokerBrief}` : "", intentTag, modeTag].filter(Boolean).join(" ");
1167
+ const routeInput: AdvisorRouteInput = { phase: "preflight", text: enrichedText || prompt || event.systemPrompt || briefText || brokerBrief || intentTag || modeTag || "", brief: [briefText, brokerBrief].filter(Boolean).join("\n\n") };
1150
1168
 
1151
1169
  // Binary gate model — fast local classifier for continue/escalate decisions
1152
1170
  const gatePrediction = binaryGatePredict(routeInput.text);
@@ -1191,6 +1209,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
1191
1209
  note,
1192
1210
  controlTag,
1193
1211
  briefText ? `Brief (cache-aware):\n${briefText}` : "",
1212
+ brokerBrief ? `Context broker brief (lookup-first):\n${brokerBrief}` : "",
1194
1213
  ].filter(Boolean).join("\n\n"),
1195
1214
  };
1196
1215
  });
@@ -93,6 +93,7 @@ describe("advisor two-agent convergence", () => {
93
93
  let messageRenderers: MessageRendererMap;
94
94
  let sendMessageMock: ReturnType<typeof vi.fn>;
95
95
  let completeSimpleMock: ReturnType<typeof vi.fn>;
96
+ let piMock: any;
96
97
  let priorState: string | null = null;
97
98
  let priorConfig: string | null = null;
98
99
  let priorCache: string | null = null;
@@ -107,6 +108,7 @@ describe("advisor two-agent convergence", () => {
107
108
  commands = setup.commands;
108
109
  messageRenderers = setup.messageRenderers;
109
110
  sendMessageMock = setup.sendMessage;
111
+ piMock = setup.pi;
110
112
 
111
113
  mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
112
114
  writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
@@ -377,6 +379,21 @@ describe("advisor two-agent convergence", () => {
377
379
  );
378
380
  });
379
381
 
382
+ it("includes broker briefs in manual advisor context when available", async () => {
383
+ expect(commands.advisor).toBeTruthy();
384
+ piMock.__piRogueContextBroker = {
385
+ renderBrief: () => "## Context Broker\nHot:\n- ctx://session/s/tool_output/abc/ctx-1 summary=\"npm test passed\"",
386
+ };
387
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "Use the broker handle as evidence." }] });
388
+
389
+ await commands.advisor.handler("should we use broker context", ctx);
390
+
391
+ const messages = completeSimpleMock.mock.calls.at(-1)?.[1]?.messages;
392
+ const promptText = JSON.stringify(messages ?? completeSimpleMock.mock.calls.at(-1));
393
+ expect(promptText).toContain("Context broker brief");
394
+ expect(promptText).toContain("ctx://session/s/tool_output/abc/ctx-1");
395
+ });
396
+
380
397
  it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
381
398
  const preflight = handlers.before_agent_start;
382
399
  const agentEnd = handlers.agent_end;
@@ -0,0 +1,44 @@
1
+ # Pi-Rogue Context Broker
2
+
3
+ Beta context broker runtime for Pi-Rogue.
4
+
5
+ This package contains the executable in-memory bounded broker implementation:
6
+
7
+ - `createInMemoryContextBroker()` stores artifacts behind stable `ctx://...` handles.
8
+ - Lookups support handle, session, kind, tag, path, command prefix, branch, tier, and text filters.
9
+ - Omitted summaries become metadata-only placeholders, keeping raw payloads out of prompt briefs by default.
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, tier-specific record/byte caps, TTL expiry on reads, and pinned-artifact retention.
12
+
13
+ It is intentionally disabled by default in the bundle.
14
+
15
+ ## Opt-in beta extension
16
+
17
+ Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi with the bundle installed to enable the beta extension:
18
+
19
+ ```bash
20
+ PI_CONTEXT_BROKER_ENABLED=true pi
21
+ ```
22
+
23
+ When enabled, the bundle registers a `context_lookup` LLM tool plus `/context` commands:
24
+
25
+ - `/context status` — enabled state, record/byte counts, pinned counts.
26
+ - `/context brief` — bounded prompt-safe broker brief with handles and summaries.
27
+ - `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
28
+ - `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
29
+ - `/context prune` — run TTL/cap pruning immediately.
30
+
31
+ 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
+ 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
+ ## Session behavior and limits
36
+
37
+ - On session start/reload, the beta backfills the current Pi session branch from `toolResult` and prompt-visible `bashExecution` entries.
38
+ - Backfill is idempotent by session entry id, skips malformed entries instead of failing the session, and honors Pi's `excludeFromContext` bash entries.
39
+ - 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 large `toolResult` and prompt-visible `bashExecution` payloads in the LLM-bound message copy to broker handles and summaries, reducing prompt load while preserving exact `/context lookup` rehydration.
42
+ - Pi `excludeFromContext` bash entries are not backfilled or rewritten into broker prompts.
43
+ - Basic secret redaction runs before broker storage and display for common token/password/API-key patterns.
44
+ - Rollback is immediate: unset `PI_CONTEXT_BROKER_ENABLED` and `/reload` or restart Pi. Disable durable writes by unsetting `PI_CONTEXT_BROKER_DURABLE` and `PI_CONTEXT_BROKER_STORE_DIR`.
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@fiale-plus/pi-rogue-context-broker",
3
+ "version": "0.1.0",
4
+ "description": "Beta context broker runtime for Pi-Rogue. In-memory bounded broker implementation behind explicit opt-in.",
5
+ "private": true,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "pi-package"
10
+ ],
11
+ "scripts": {
12
+ "check": "tsc -p ../../tsconfig.json --noEmit",
13
+ "test": "cd ../.. && vitest run packages/context-broker/src/*.test.ts"
14
+ },
15
+ "main": "./src/index.ts",
16
+ "exports": {
17
+ ".": "./src/index.ts",
18
+ "./extension": "./src/extension.ts",
19
+ "./file": "./src/file.ts",
20
+ "./sqlite": "./src/sqlite.ts"
21
+ },
22
+ "dependencies": {
23
+ "@fiale-plus/pi-core": "^0.1.0",
24
+ "typebox": "^1.1.24"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "README.md",
29
+ "package.json"
30
+ ]
31
+ }