@fiale-plus/pi-rogue 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/node_modules/@fiale-plus/pi-core/README.md +13 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +749 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +51 -0
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/context-broker.ts +1 -0
- package/src/extension.test.ts +68 -0
- package/src/extension.ts +27 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { safeName } from "@fiale-plus/pi-core";
|
|
5
|
+
import type { BoundedContextBroker, ContextArtifact, ContextArtifactInput, ContextArtifactTier, ContextBrokerOptions, ContextBrokerStatus, ContextLookupQuery, ContextPurgeOptions } from "@fiale-plus/pi-core";
|
|
6
|
+
import { createInMemoryContextBroker } from "./index.js";
|
|
7
|
+
|
|
8
|
+
export interface FileContextBrokerOptions extends ContextBrokerOptions {
|
|
9
|
+
dir?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const STORE_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
interface StoredRecord {
|
|
15
|
+
version: number;
|
|
16
|
+
handle: string;
|
|
17
|
+
baseTier?: ContextArtifactTier;
|
|
18
|
+
input: Omit<ContextArtifactInput, "payload"> & { payloadSha256: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultStoreDir(): string {
|
|
22
|
+
return join(homedir(), ".pi", "agent", "fiale-plus", "context-broker");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureDir(path: string): void {
|
|
26
|
+
mkdirSync(path, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function metadataFile(dir: string): string {
|
|
30
|
+
return join(dir, "metadata.jsonl");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function blobFile(dir: string, sha256: string): string {
|
|
34
|
+
return join(dir, "blobs", `${sha256}.txt`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stableSource(input: ContextArtifactInput): string | undefined {
|
|
38
|
+
return input.parentIds?.find(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStoredRecords(dir: string): StoredRecord[] {
|
|
42
|
+
const file = metadataFile(dir);
|
|
43
|
+
if (!existsSync(file)) return [];
|
|
44
|
+
const recordsByHandle = new Map<string, StoredRecord>();
|
|
45
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed) continue;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(trimmed) as StoredRecord;
|
|
50
|
+
if (parsed?.version === STORE_VERSION && parsed.handle && parsed.input?.payloadSha256) {
|
|
51
|
+
recordsByHandle.set(parsed.handle, parsed);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore corrupt JSONL rows; durable storage is append-only recovery, not a startup blocker.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...recordsByHandle.values()];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadPayload(dir: string, sha256: string): string | undefined {
|
|
61
|
+
const file = blobFile(dir, sha256);
|
|
62
|
+
if (!existsSync(file)) return undefined;
|
|
63
|
+
return readFileSync(file, "utf8");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function artifactBaseTier(artifact: ContextArtifact, fallback?: ContextArtifactTier): ContextArtifactTier {
|
|
67
|
+
return (artifact as ContextArtifact & { baseTier?: ContextArtifactTier }).baseTier ?? fallback ?? artifact.tier;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function persistRecord(dir: string, artifact: ContextArtifact, input: ContextArtifactInput): void {
|
|
71
|
+
ensureDir(join(dir, "blobs"));
|
|
72
|
+
const blob = blobFile(dir, artifact.sha256);
|
|
73
|
+
if (!existsSync(blob)) writeFileSync(blob, artifact.payload, "utf8");
|
|
74
|
+
const record: StoredRecord = {
|
|
75
|
+
version: STORE_VERSION,
|
|
76
|
+
handle: artifact.handle,
|
|
77
|
+
baseTier: artifactBaseTier(artifact, input.tier),
|
|
78
|
+
input: {
|
|
79
|
+
sessionId: input.sessionId,
|
|
80
|
+
kind: input.kind,
|
|
81
|
+
summary: input.summary,
|
|
82
|
+
tags: input.tags,
|
|
83
|
+
paths: input.paths,
|
|
84
|
+
command: input.command,
|
|
85
|
+
branch: input.branch,
|
|
86
|
+
tier: artifactBaseTier(artifact, input.tier),
|
|
87
|
+
ttlMs: input.ttlMs,
|
|
88
|
+
pinned: artifact.pinned,
|
|
89
|
+
parentIds: input.parentIds,
|
|
90
|
+
createdAt: input.createdAt ?? artifact.createdAt,
|
|
91
|
+
payloadSha256: artifact.sha256,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
ensureDir(dirname(metadataFile(dir)));
|
|
95
|
+
appendFileSync(metadataFile(dir), `${JSON.stringify(record)}\n`, "utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function persistArtifactSnapshot(dir: string, artifact: ContextArtifact): void {
|
|
99
|
+
persistRecord(dir, artifact, {
|
|
100
|
+
sessionId: artifact.sessionId,
|
|
101
|
+
kind: artifact.kind,
|
|
102
|
+
payload: artifact.payload,
|
|
103
|
+
summary: artifact.summary,
|
|
104
|
+
tags: artifact.tags,
|
|
105
|
+
paths: artifact.paths,
|
|
106
|
+
command: artifact.command,
|
|
107
|
+
branch: artifact.branch,
|
|
108
|
+
tier: artifactBaseTier(artifact),
|
|
109
|
+
ttlMs: artifact.expiresAt ? Math.max(0, artifact.expiresAt - artifact.createdAt) : 0,
|
|
110
|
+
pinned: artifact.pinned,
|
|
111
|
+
parentIds: artifact.parentIds,
|
|
112
|
+
createdAt: artifact.createdAt,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function removeUnreferencedBlobs(dir: string, keptSha256: Set<string>): void {
|
|
117
|
+
const blobsDir = join(dir, "blobs");
|
|
118
|
+
if (!existsSync(blobsDir)) return;
|
|
119
|
+
for (const entry of readdirSync(blobsDir)) {
|
|
120
|
+
if (!entry.endsWith(".txt")) continue;
|
|
121
|
+
const sha256 = entry.slice(0, -4);
|
|
122
|
+
if (!keptSha256.has(sha256)) unlinkSync(join(blobsDir, entry));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createFileContextBroker(options: FileContextBrokerOptions = {}): BoundedContextBroker {
|
|
127
|
+
const dir = options.dir ?? process.env.PI_CONTEXT_BROKER_STORE_DIR ?? defaultStoreDir();
|
|
128
|
+
ensureDir(join(dir, "blobs"));
|
|
129
|
+
const broker = createInMemoryContextBroker(options);
|
|
130
|
+
const persistedSources = new Map<string, string>();
|
|
131
|
+
const handleAliases = new Map<string, string>();
|
|
132
|
+
|
|
133
|
+
for (const record of readStoredRecords(dir)) {
|
|
134
|
+
const payload = loadPayload(dir, record.input.payloadSha256);
|
|
135
|
+
if (payload === undefined) continue;
|
|
136
|
+
const artifact = broker.publish({ ...record.input, tier: record.baseTier ?? record.input.tier, payload });
|
|
137
|
+
handleAliases.set(record.handle, artifact.handle);
|
|
138
|
+
const source = stableSource(record.input as unknown as ContextArtifactInput);
|
|
139
|
+
if (source) persistedSources.set(source, artifact.handle);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function publish(input: ContextArtifactInput): ContextArtifact {
|
|
143
|
+
const source = stableSource(input);
|
|
144
|
+
const existingHandle = source ? persistedSources.get(source) : undefined;
|
|
145
|
+
if (existingHandle) {
|
|
146
|
+
const existing = broker.lookup({ handle: existingHandle })[0];
|
|
147
|
+
if (existing) return existing;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const artifact = broker.publish(input);
|
|
151
|
+
if (source) persistedSources.set(source, artifact.handle);
|
|
152
|
+
persistRecord(dir, artifact, input);
|
|
153
|
+
return artifact;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
publish,
|
|
158
|
+
lookup(query?: ContextLookupQuery): ContextArtifact[] {
|
|
159
|
+
const mappedHandle = query?.handle ? handleAliases.get(query.handle) : undefined;
|
|
160
|
+
return broker.lookup(mappedHandle ? { ...query, handle: mappedHandle } : query);
|
|
161
|
+
},
|
|
162
|
+
pin(idOrHandle: string, pinned?: boolean): ContextArtifact | null {
|
|
163
|
+
const artifact = broker.pin(handleAliases.get(idOrHandle) ?? idOrHandle, pinned);
|
|
164
|
+
if (artifact) persistArtifactSnapshot(dir, artifact);
|
|
165
|
+
return artifact;
|
|
166
|
+
},
|
|
167
|
+
prune(now?: number): ContextBrokerStatus { return broker.prune(now); },
|
|
168
|
+
purge(options?: ContextPurgeOptions): ContextBrokerStatus {
|
|
169
|
+
const status = broker.purge(options);
|
|
170
|
+
const remaining = broker.lookup({ limit: Number.MAX_SAFE_INTEGER });
|
|
171
|
+
persistedSources.clear();
|
|
172
|
+
handleAliases.clear();
|
|
173
|
+
writeFileSync(metadataFile(dir), "", "utf8");
|
|
174
|
+
const keptSha256 = new Set<string>();
|
|
175
|
+
for (const artifact of remaining) {
|
|
176
|
+
keptSha256.add(artifact.sha256);
|
|
177
|
+
for (const parentId of artifact.parentIds) persistedSources.set(parentId, artifact.handle);
|
|
178
|
+
handleAliases.set(artifact.handle, artifact.handle);
|
|
179
|
+
persistArtifactSnapshot(dir, artifact);
|
|
180
|
+
}
|
|
181
|
+
removeUnreferencedBlobs(dir, keptSha256);
|
|
182
|
+
return status;
|
|
183
|
+
},
|
|
184
|
+
status(): ContextBrokerStatus { return broker.status(); },
|
|
185
|
+
renderBrief(query?: ContextLookupQuery & { budgetBytes?: number }): string { return broker.renderBrief(query); },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function contextBrokerStoreDirForSession(baseDir: string, sessionId: string): string {
|
|
190
|
+
return join(baseDir, safeName(sessionId));
|
|
191
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createInMemoryContextBroker } from "./index.js";
|
|
4
|
+
|
|
5
|
+
describe("createInMemoryContextBroker", () => {
|
|
6
|
+
it("publishes stable, unique handles and looks up artifacts by handle", () => {
|
|
7
|
+
const broker = createInMemoryContextBroker();
|
|
8
|
+
const first = broker.publish({
|
|
9
|
+
sessionId: "session-a",
|
|
10
|
+
kind: "tool_output",
|
|
11
|
+
payload: "same payload",
|
|
12
|
+
summary: "tests passed",
|
|
13
|
+
tags: ["test"],
|
|
14
|
+
paths: ["packages/core"],
|
|
15
|
+
});
|
|
16
|
+
const second = broker.publish({
|
|
17
|
+
sessionId: "session-a",
|
|
18
|
+
kind: "tool_output",
|
|
19
|
+
payload: "same payload",
|
|
20
|
+
summary: "same payload repeat",
|
|
21
|
+
tags: ["test"],
|
|
22
|
+
paths: ["packages/core"],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(first.handle).not.toEqual(second.handle);
|
|
26
|
+
expect(first.handle).toMatch(/^ctx:\/\/session\/session-a\/tool_output\//);
|
|
27
|
+
expect(broker.lookup({ handle: first.handle })).toEqual([first]);
|
|
28
|
+
expect(broker.lookup({ handle: second.handle })).toEqual([second]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("filters by session, kind, tag, path, and text", () => {
|
|
32
|
+
const broker = createInMemoryContextBroker();
|
|
33
|
+
const core = broker.publish({
|
|
34
|
+
sessionId: "s1",
|
|
35
|
+
kind: "tool_output",
|
|
36
|
+
payload: "vitest packages/core passed",
|
|
37
|
+
tags: ["test", "core"],
|
|
38
|
+
paths: ["packages/core/src/context-broker.ts"],
|
|
39
|
+
});
|
|
40
|
+
broker.publish({
|
|
41
|
+
sessionId: "s2",
|
|
42
|
+
kind: "advisor_brief",
|
|
43
|
+
payload: "different payload",
|
|
44
|
+
tags: ["advisor"],
|
|
45
|
+
paths: ["packages/advisor/src/router.ts"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(broker.lookup({ sessionId: "s1", kind: "tool_output", tag: "core" })).toEqual([core]);
|
|
49
|
+
expect(broker.lookup({ path: "packages/core" })).toEqual([core]);
|
|
50
|
+
expect(broker.lookup({ text: "vitest" })).toEqual([core]);
|
|
51
|
+
expect(broker.lookup({ sessionId: "s2", kind: "tool_output" })).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses a metadata-only summary when callers omit summaries", () => {
|
|
55
|
+
const broker = createInMemoryContextBroker({ briefBytes: 500 });
|
|
56
|
+
const artifact = broker.publish({
|
|
57
|
+
sessionId: "s",
|
|
58
|
+
kind: "tool_output",
|
|
59
|
+
payload: "SECRET_TOKEN=abc123\n".repeat(20),
|
|
60
|
+
paths: ["logs/secret-output.txt"],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const brief = broker.renderBrief({ sessionId: "s" });
|
|
64
|
+
|
|
65
|
+
expect(artifact.summary).toContain("payload stored externally");
|
|
66
|
+
expect(artifact.summary).not.toContain("SECRET_TOKEN");
|
|
67
|
+
expect(brief).not.toContain("SECRET_TOKEN");
|
|
68
|
+
expect(brief).toContain(artifact.handle);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("enforces record caps by pruning oldest unpinned artifacts", () => {
|
|
72
|
+
const broker = createInMemoryContextBroker({ maxRecords: 2, defaultTtlMs: 0 });
|
|
73
|
+
const first = broker.publish({ sessionId: "s", kind: "memory_note", payload: "one", createdAt: 1 });
|
|
74
|
+
const second = broker.publish({ sessionId: "s", kind: "memory_note", payload: "two", createdAt: 2 });
|
|
75
|
+
const third = broker.publish({ sessionId: "s", kind: "memory_note", payload: "three", createdAt: 3 });
|
|
76
|
+
|
|
77
|
+
expect(broker.lookup({ id: first.id })).toEqual([]);
|
|
78
|
+
expect(broker.lookup({ id: second.id })).toEqual([second]);
|
|
79
|
+
expect(broker.lookup({ id: third.id })).toEqual([third]);
|
|
80
|
+
expect(broker.status().records).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("applies caps independently per session", () => {
|
|
84
|
+
const broker = createInMemoryContextBroker({ maxRecords: 1, defaultTtlMs: 0 });
|
|
85
|
+
const sessionOne = broker.publish({ sessionId: "s1", kind: "tool_output", payload: "one", createdAt: 1 });
|
|
86
|
+
const sessionTwo = broker.publish({ sessionId: "s2", kind: "tool_output", payload: "two", createdAt: 2 });
|
|
87
|
+
|
|
88
|
+
expect(broker.lookup({ sessionId: "s1" })).toEqual([sessionOne]);
|
|
89
|
+
expect(broker.lookup({ sessionId: "s2" })).toEqual([sessionTwo]);
|
|
90
|
+
expect(broker.status().records).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("uses sequence tie-breakers when createdAt timestamps tie", () => {
|
|
94
|
+
const broker = createInMemoryContextBroker({ maxRecords: 2, defaultTtlMs: 0 });
|
|
95
|
+
const first = broker.publish({ sessionId: "s", kind: "tool_output", payload: "alpha", createdAt: 1000 });
|
|
96
|
+
const second = broker.publish({ sessionId: "s", kind: "tool_output", payload: "bravo", createdAt: 1000 });
|
|
97
|
+
const third = broker.publish({ sessionId: "s", kind: "tool_output", payload: "charlie", createdAt: 1000 });
|
|
98
|
+
|
|
99
|
+
expect(broker.lookup({ id: first.id })).toEqual([]);
|
|
100
|
+
expect(broker.lookup({ id: second.id })).toEqual([second]);
|
|
101
|
+
expect(broker.lookup({ id: third.id })).toEqual([third]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("enforces byte caps by pruning oldest unpinned artifacts", () => {
|
|
105
|
+
const broker = createInMemoryContextBroker({ maxBytes: 6, defaultTtlMs: 0 });
|
|
106
|
+
const first = broker.publish({ sessionId: "s", kind: "tool_output", payload: "12345", createdAt: 1 });
|
|
107
|
+
const second = broker.publish({ sessionId: "s", kind: "tool_output", payload: "abcde", createdAt: 2 });
|
|
108
|
+
|
|
109
|
+
expect(broker.lookup({ id: first.id })).toEqual([]);
|
|
110
|
+
expect(broker.lookup({ id: second.id })).toEqual([second]);
|
|
111
|
+
expect(broker.status().bytes).toBe(5);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("preserves the returned handle when a new artifact exceeds maxBytes", () => {
|
|
115
|
+
const broker = createInMemoryContextBroker({ maxBytes: 4, defaultTtlMs: 0 });
|
|
116
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload: "oversized", createdAt: 1 });
|
|
117
|
+
|
|
118
|
+
expect(broker.lookup({ id: artifact.id })).toEqual([artifact]);
|
|
119
|
+
expect(broker.lookup({ handle: artifact.handle })).toEqual([artifact]);
|
|
120
|
+
expect(broker.status().bytes).toBe(Buffer.byteLength("oversized", "utf8"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("computes bytes and SHA-256 on raw Buffer payload bytes", () => {
|
|
124
|
+
const broker = createInMemoryContextBroker({ maxBytes: 1024, defaultTtlMs: 0 });
|
|
125
|
+
const payload = Buffer.from([0x66, 0xff, 0x61, 0x62, 0x80, 0x00]);
|
|
126
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload, createdAt: 1 });
|
|
127
|
+
const expectedSha = createHash("sha256").update(payload).digest("hex");
|
|
128
|
+
|
|
129
|
+
expect(artifact.bytes).toBe(payload.length);
|
|
130
|
+
expect(artifact.sha256).toBe(expectedSha);
|
|
131
|
+
expect(Buffer.byteLength(artifact.payload, "utf8")).toBeGreaterThan(artifact.bytes);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keeps pinned artifacts visible while pruning unpinned records", () => {
|
|
135
|
+
const broker = createInMemoryContextBroker({ maxRecords: 2, defaultTtlMs: 0 });
|
|
136
|
+
const pinned = broker.publish({ sessionId: "s", kind: "diff", payload: "important", pinned: true, createdAt: 1 });
|
|
137
|
+
const older = broker.publish({ sessionId: "s", kind: "diff", payload: "temporary", createdAt: 2 });
|
|
138
|
+
const newer = broker.publish({ sessionId: "s", kind: "diff", payload: "latest", createdAt: 3 });
|
|
139
|
+
|
|
140
|
+
expect(broker.lookup({ id: pinned.id })).toEqual([pinned]);
|
|
141
|
+
expect(broker.lookup({ id: older.id })).toEqual([]);
|
|
142
|
+
expect(broker.lookup({ id: newer.id })).toEqual([newer]);
|
|
143
|
+
expect(broker.status().pinnedRecords).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("expires unpinned artifacts by ttl", () => {
|
|
147
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 10 });
|
|
148
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload: "old", createdAt: 100 });
|
|
149
|
+
|
|
150
|
+
broker.prune(111);
|
|
151
|
+
|
|
152
|
+
expect(broker.lookup({ id: artifact.id })).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("prunes expired artifacts before lookup without an explicit prune call", () => {
|
|
156
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 1 });
|
|
157
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload: "expired", createdAt: 1 });
|
|
158
|
+
|
|
159
|
+
expect(broker.lookup({ id: artifact.id })).toEqual([]);
|
|
160
|
+
expect(broker.status().records).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("omits expired artifacts from rendered prompt briefs", () => {
|
|
164
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 1, briefBytes: 500 });
|
|
165
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload: "expired secret", summary: "expired summary", createdAt: 1 });
|
|
166
|
+
|
|
167
|
+
const brief = broker.renderBrief({ sessionId: "s" });
|
|
168
|
+
|
|
169
|
+
expect(brief).not.toContain(artifact.handle);
|
|
170
|
+
expect(brief).not.toContain("expired summary");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("keeps pinned expired artifacts visible until unpinned", () => {
|
|
174
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 1 });
|
|
175
|
+
const artifact = broker.publish({ sessionId: "s", kind: "tool_output", payload: "pinned", pinned: true, createdAt: 1 });
|
|
176
|
+
|
|
177
|
+
expect(broker.lookup({ id: artifact.id })).toEqual([artifact]);
|
|
178
|
+
expect(broker.pin(artifact.id, false)).toBeNull();
|
|
179
|
+
expect(broker.lookup({ id: artifact.id })).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("classifies artifacts into hot, warm, and cold tiers on publish", () => {
|
|
183
|
+
const broker = createInMemoryContextBroker({ defaultTtlMs: 0 });
|
|
184
|
+
const failure = broker.publish({ sessionId: "s", kind: "tool_output", payload: "failed", tags: ["error"], createdAt: 1 });
|
|
185
|
+
const command = broker.publish({ sessionId: "s", kind: "tool_output", payload: "passed", tags: ["ok"], createdAt: 2 });
|
|
186
|
+
const archive = broker.publish({ sessionId: "s", kind: "subagent_result", payload: "old", tags: ["completed"], createdAt: 3 });
|
|
187
|
+
const explicit = broker.publish({ sessionId: "s", kind: "diff", payload: "manual", tier: "cold", createdAt: 4 });
|
|
188
|
+
|
|
189
|
+
expect(failure.tier).toBe("hot");
|
|
190
|
+
expect(command.tier).toBe("warm");
|
|
191
|
+
expect(archive.tier).toBe("cold");
|
|
192
|
+
expect(explicit.tier).toBe("cold");
|
|
193
|
+
expect(broker.lookup({ sessionId: "s", tier: "cold" }).map((artifact) => artifact.id)).toEqual([explicit.id, archive.id]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("renders prompt briefs hot-first, warm-second, and excludes cold unless explicit", () => {
|
|
197
|
+
const broker = createInMemoryContextBroker({ briefBytes: 900, defaultTtlMs: 0 });
|
|
198
|
+
const cold = broker.publish({ sessionId: "s", kind: "tool_output", payload: "cold", summary: "cold archive", tier: "cold", createdAt: 1 });
|
|
199
|
+
const warm = broker.publish({ sessionId: "s", kind: "tool_output", payload: "warm", summary: "warm command", tier: "warm", createdAt: 2 });
|
|
200
|
+
const hot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "hot", summary: "hot failure", tier: "hot", createdAt: 3 });
|
|
201
|
+
|
|
202
|
+
const brief = broker.renderBrief({ sessionId: "s" });
|
|
203
|
+
expect(brief).toContain("Hot:");
|
|
204
|
+
expect(brief).toContain(hot.handle);
|
|
205
|
+
expect(brief).toContain("Warm:");
|
|
206
|
+
expect(brief).toContain(warm.handle);
|
|
207
|
+
expect(brief).not.toContain(cold.handle);
|
|
208
|
+
expect(brief.indexOf(hot.handle)).toBeLessThan(brief.indexOf(warm.handle));
|
|
209
|
+
|
|
210
|
+
const coldBrief = broker.renderBrief({ sessionId: "s", tier: "cold", budgetBytes: 500 });
|
|
211
|
+
expect(coldBrief).toContain("Cold:");
|
|
212
|
+
expect(coldBrief).toContain(cold.handle);
|
|
213
|
+
|
|
214
|
+
expect(broker.pin(cold.handle, true)?.tier).toBe("hot");
|
|
215
|
+
expect(broker.renderBrief({ sessionId: "s" })).toContain(cold.handle);
|
|
216
|
+
expect(broker.pin(cold.handle, false)?.tier).toBe("cold");
|
|
217
|
+
expect(broker.renderBrief({ sessionId: "s" })).not.toContain(cold.handle);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("applies tier-specific record, byte, and ttl retention", () => {
|
|
221
|
+
const broker = createInMemoryContextBroker({
|
|
222
|
+
defaultTtlMs: 0,
|
|
223
|
+
hotMaxRecords: 1,
|
|
224
|
+
warmMaxBytes: 6,
|
|
225
|
+
coldTtlMs: 10,
|
|
226
|
+
});
|
|
227
|
+
const oldHot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "old-hot", tier: "hot", createdAt: 1 });
|
|
228
|
+
const newHot = broker.publish({ sessionId: "s", kind: "tool_output", payload: "new-hot", tier: "hot", createdAt: 2 });
|
|
229
|
+
const oldWarm = broker.publish({ sessionId: "s", kind: "tool_output", payload: "12345", tier: "warm", createdAt: 3 });
|
|
230
|
+
const newWarm = broker.publish({ sessionId: "s", kind: "tool_output", payload: "abcde", tier: "warm", createdAt: 4 });
|
|
231
|
+
const cold = broker.publish({ sessionId: "s", kind: "tool_output", payload: "cold", tier: "cold", createdAt: 5 });
|
|
232
|
+
|
|
233
|
+
expect(broker.lookup({ id: oldHot.id })).toEqual([]);
|
|
234
|
+
expect(broker.lookup({ id: newHot.id })).toEqual([newHot]);
|
|
235
|
+
expect(broker.lookup({ id: oldWarm.id })).toEqual([]);
|
|
236
|
+
expect(broker.lookup({ id: newWarm.id })).toEqual([newWarm]);
|
|
237
|
+
|
|
238
|
+
broker.prune(16);
|
|
239
|
+
expect(broker.lookup({ id: cold.id })).toEqual([]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("renders a bounded prompt brief with lookup instructions", () => {
|
|
243
|
+
const broker = createInMemoryContextBroker({ briefBytes: 180 });
|
|
244
|
+
broker.publish({
|
|
245
|
+
sessionId: "s",
|
|
246
|
+
kind: "tool_output",
|
|
247
|
+
payload: "x".repeat(500),
|
|
248
|
+
summary: "large command output passed with no failures",
|
|
249
|
+
tags: ["test"],
|
|
250
|
+
paths: ["packages/core"],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const brief = broker.renderBrief();
|
|
254
|
+
|
|
255
|
+
expect(Buffer.byteLength(brief, "utf8")).toBeLessThanOrEqual(180);
|
|
256
|
+
expect(brief).toContain("Context Broker");
|
|
257
|
+
expect(brief).toContain("ctx://session/s/tool_output/");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("enforces prompt brief budgets by UTF-8 byte length", () => {
|
|
261
|
+
const broker = createInMemoryContextBroker({ briefBytes: 170 });
|
|
262
|
+
broker.publish({
|
|
263
|
+
sessionId: "emoji-session",
|
|
264
|
+
kind: "tool_output",
|
|
265
|
+
payload: "✅".repeat(200),
|
|
266
|
+
summary: "✅ 測試 passed ".repeat(20),
|
|
267
|
+
tags: ["測試", "✅"],
|
|
268
|
+
paths: ["packages/核心/✅.ts"],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const brief = broker.renderBrief({ budgetBytes: 170 });
|
|
272
|
+
|
|
273
|
+
expect(Buffer.byteLength(brief, "utf8")).toBeLessThanOrEqual(170);
|
|
274
|
+
expect(brief).toContain("Context Broker");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("purges unpinned session artifacts while retaining pinned evidence", () => {
|
|
278
|
+
const broker = createInMemoryContextBroker();
|
|
279
|
+
const unpinned = broker.publish({ sessionId: "s", kind: "tool_output", payload: "scratch", summary: "scratch" });
|
|
280
|
+
const pinned = broker.publish({ sessionId: "s", kind: "tool_output", payload: "keep", summary: "keep", pinned: true });
|
|
281
|
+
const other = broker.publish({ sessionId: "other", kind: "tool_output", payload: "other", summary: "other" });
|
|
282
|
+
|
|
283
|
+
const status = broker.purge({ sessionId: "s", keepPinned: true });
|
|
284
|
+
|
|
285
|
+
expect(status.records).toBe(2);
|
|
286
|
+
expect(broker.lookup({ handle: unpinned.handle })).toEqual([]);
|
|
287
|
+
expect(broker.lookup({ handle: pinned.handle })[0]?.payload).toBe("keep");
|
|
288
|
+
expect(broker.lookup({ handle: other.handle })[0]?.payload).toBe("other");
|
|
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
|
+
});
|
|
302
|
+
});
|