@fiale-plus/pi-rogue-bundle 0.1.16 → 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 +2 -1
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +20 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +26 -7
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +17 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +12 -6
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +5 -2
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +262 -3
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +242 -28
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +165 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +60 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +99 -20
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +78 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +500 -0
- package/package.json +4 -2
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/extension.test.ts +5 -0
- package/src/extension.ts +3 -3
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ContextArtifact,
|
|
6
6
|
ContextArtifactInput,
|
|
7
7
|
ContextArtifactKind,
|
|
8
|
+
ContextArtifactTier,
|
|
8
9
|
ContextBrokerOptions,
|
|
9
10
|
ContextBrokerStatus,
|
|
10
11
|
ContextLookupQuery,
|
|
@@ -15,6 +16,7 @@ export type {
|
|
|
15
16
|
ContextArtifact,
|
|
16
17
|
ContextArtifactInput,
|
|
17
18
|
ContextArtifactKind,
|
|
19
|
+
ContextArtifactTier,
|
|
18
20
|
ContextBrokerOptions,
|
|
19
21
|
ContextBrokerStatus,
|
|
20
22
|
ContextLookupQuery,
|
|
@@ -25,6 +27,8 @@ const DEFAULT_MAX_BYTES = 128 * 1024 * 1024;
|
|
|
25
27
|
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
26
28
|
const DEFAULT_SUMMARY_BYTES = 320;
|
|
27
29
|
const DEFAULT_BRIEF_BYTES = 2_000;
|
|
30
|
+
const TIER_ORDER: Record<ContextArtifactTier, number> = { hot: 0, warm: 1, cold: 2 };
|
|
31
|
+
const TIER_REMOVAL_ORDER: Record<ContextArtifactTier, number> = { cold: 0, warm: 1, hot: 2 };
|
|
28
32
|
|
|
29
33
|
function normalizeList(values: string[] | undefined): string[] {
|
|
30
34
|
return [...new Set((values ?? []).map((value) => String(value || "").trim()).filter(Boolean))];
|
|
@@ -74,12 +78,25 @@ function summarizeArtifact(summary: string | undefined, kind: ContextArtifactKin
|
|
|
74
78
|
return truncateUtf8(`[${kind} payload stored externally; ${bytes} bytes; sha256=${sha256.slice(0, 16)}]`, maxBytes);
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
function classifyBaseTier(input: ContextArtifactInput, tags: string[]): ContextArtifactTier {
|
|
82
|
+
if (input.tier) return input.tier;
|
|
83
|
+
const normalizedTags = tags.map((tag) => tag.toLowerCase());
|
|
84
|
+
if (normalizedTags.includes("hot")) return "hot";
|
|
85
|
+
if (normalizedTags.includes("warm")) return "warm";
|
|
86
|
+
if (normalizedTags.includes("cold")) return "cold";
|
|
87
|
+
if (normalizedTags.some((tag) => tag === "error" || tag === "failed" || tag === "failure")) return "hot";
|
|
88
|
+
if (normalizedTags.some((tag) => tag === "archive" || tag === "historical" || tag === "completed")) return "cold";
|
|
89
|
+
if (input.kind === "advisor_brief" || input.kind === "memory_note") return "hot";
|
|
90
|
+
return "warm";
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
function artifactMatches(artifact: ContextArtifact, query: ContextLookupQuery): boolean {
|
|
78
94
|
if (query.id && artifact.id !== query.id) return false;
|
|
79
95
|
if (query.handle && artifact.handle !== query.handle) return false;
|
|
80
96
|
if (query.sessionId && artifact.sessionId !== query.sessionId) return false;
|
|
81
97
|
if (query.kind && artifact.kind !== query.kind) return false;
|
|
82
98
|
if (query.branch && artifact.branch !== query.branch) return false;
|
|
99
|
+
if (query.tier && artifact.tier !== query.tier) return false;
|
|
83
100
|
if (query.tag && !artifact.tags.includes(query.tag)) return false;
|
|
84
101
|
if (query.path) {
|
|
85
102
|
const queryPath = query.path.replace(/\/$/, "");
|
|
@@ -98,23 +115,55 @@ function artifactMatches(artifact: ContextArtifact, query: ContextLookupQuery):
|
|
|
98
115
|
return true;
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
function tierLine(artifact: ContextArtifact): string {
|
|
119
|
+
const pin = artifact.pinned ? " pinned" : "";
|
|
120
|
+
const path = artifact.paths.length ? ` paths=${artifact.paths.slice(0, 3).join(",")}` : "";
|
|
121
|
+
const tags = artifact.tags.length ? ` tags=${artifact.tags.slice(0, 3).join(",")}` : "";
|
|
122
|
+
return `- ${artifact.handle} tier=${artifact.tier} kind=${artifact.kind}${pin}${path}${tags} summary="${artifact.summary}"`;
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
export function createInMemoryContextBroker(options: ContextBrokerOptions = {}): BoundedContextBroker {
|
|
102
126
|
const maxRecords = Math.max(1, Math.floor(options.maxRecords ?? DEFAULT_MAX_RECORDS));
|
|
103
127
|
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
104
128
|
const defaultTtlMs = Math.max(0, Math.floor(options.defaultTtlMs ?? DEFAULT_TTL_MS));
|
|
129
|
+
const tierTtlMs: Record<ContextArtifactTier, number> = {
|
|
130
|
+
hot: Math.max(0, Math.floor(options.hotTtlMs ?? defaultTtlMs)),
|
|
131
|
+
warm: Math.max(0, Math.floor(options.warmTtlMs ?? defaultTtlMs)),
|
|
132
|
+
cold: Math.max(0, Math.floor(options.coldTtlMs ?? defaultTtlMs)),
|
|
133
|
+
};
|
|
134
|
+
const tierMaxRecords: Record<ContextArtifactTier, number> = {
|
|
135
|
+
hot: Math.max(1, Math.floor(options.hotMaxRecords ?? maxRecords)),
|
|
136
|
+
warm: Math.max(1, Math.floor(options.warmMaxRecords ?? maxRecords)),
|
|
137
|
+
cold: Math.max(1, Math.floor(options.coldMaxRecords ?? maxRecords)),
|
|
138
|
+
};
|
|
139
|
+
const tierMaxBytes: Record<ContextArtifactTier, number> = {
|
|
140
|
+
hot: Math.max(1, Math.floor(options.hotMaxBytes ?? maxBytes)),
|
|
141
|
+
warm: Math.max(1, Math.floor(options.warmMaxBytes ?? maxBytes)),
|
|
142
|
+
cold: Math.max(1, Math.floor(options.coldMaxBytes ?? maxBytes)),
|
|
143
|
+
};
|
|
105
144
|
const summaryBytes = Math.max(16, Math.floor(options.summaryBytes ?? DEFAULT_SUMMARY_BYTES));
|
|
106
145
|
const defaultBriefBytes = Math.max(64, Math.floor(options.briefBytes ?? DEFAULT_BRIEF_BYTES));
|
|
107
|
-
let artifacts: Array<ContextArtifact & { sequence: number }> = [];
|
|
146
|
+
let artifacts: Array<ContextArtifact & { sequence: number; baseTier: ContextArtifactTier }> = [];
|
|
108
147
|
let sequence = 0;
|
|
109
148
|
|
|
110
149
|
function currentStatus(): ContextBrokerStatus {
|
|
111
150
|
const bytes = artifacts.reduce((sum, artifact) => sum + artifact.bytes, 0);
|
|
112
151
|
const pinned = artifacts.filter((artifact) => artifact.pinned);
|
|
152
|
+
const byTier = (tier: ContextArtifactTier) => artifacts.filter((artifact) => artifact.tier === tier);
|
|
153
|
+
const hot = byTier("hot");
|
|
154
|
+
const warm = byTier("warm");
|
|
155
|
+
const cold = byTier("cold");
|
|
113
156
|
return {
|
|
114
157
|
records: artifacts.length,
|
|
115
158
|
bytes,
|
|
116
159
|
pinnedRecords: pinned.length,
|
|
117
160
|
pinnedBytes: pinned.reduce((sum, artifact) => sum + artifact.bytes, 0),
|
|
161
|
+
hotRecords: hot.length,
|
|
162
|
+
hotBytes: hot.reduce((sum, artifact) => sum + artifact.bytes, 0),
|
|
163
|
+
warmRecords: warm.length,
|
|
164
|
+
warmBytes: warm.reduce((sum, artifact) => sum + artifact.bytes, 0),
|
|
165
|
+
coldRecords: cold.length,
|
|
166
|
+
coldBytes: cold.reduce((sum, artifact) => sum + artifact.bytes, 0),
|
|
118
167
|
maxRecords,
|
|
119
168
|
maxBytes,
|
|
120
169
|
};
|
|
@@ -126,27 +175,40 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
126
175
|
);
|
|
127
176
|
}
|
|
128
177
|
|
|
129
|
-
function
|
|
178
|
+
function removalCandidates(sessionId: string, protectedIds: Set<string>, tier?: ContextArtifactTier): Array<{ artifact: ContextArtifact & { sequence: number; baseTier: ContextArtifactTier }; index: number }> {
|
|
130
179
|
return artifacts
|
|
131
180
|
.map((artifact, index) => ({ artifact, index }))
|
|
132
|
-
.filter(({ artifact }) => artifact.sessionId === sessionId && !artifact.pinned && !protectedIds.has(artifact.id))
|
|
181
|
+
.filter(({ artifact }) => artifact.sessionId === sessionId && !artifact.pinned && !protectedIds.has(artifact.id) && (!tier || artifact.tier === tier))
|
|
133
182
|
.sort((a, b) => {
|
|
183
|
+
if (!tier && TIER_REMOVAL_ORDER[a.artifact.tier] !== TIER_REMOVAL_ORDER[b.artifact.tier]) {
|
|
184
|
+
return TIER_REMOVAL_ORDER[a.artifact.tier] - TIER_REMOVAL_ORDER[b.artifact.tier];
|
|
185
|
+
}
|
|
134
186
|
if (a.artifact.createdAt !== b.artifact.createdAt) return a.artifact.createdAt - b.artifact.createdAt;
|
|
135
187
|
return a.artifact.sequence - b.artifact.sequence;
|
|
136
|
-
})
|
|
188
|
+
});
|
|
137
189
|
}
|
|
138
190
|
|
|
139
|
-
function
|
|
140
|
-
const sessionArtifacts = artifacts.filter((artifact) => artifact.sessionId === sessionId);
|
|
141
|
-
|
|
191
|
+
function withinCaps(sessionId: string, tier?: ContextArtifactTier): boolean {
|
|
192
|
+
const sessionArtifacts = artifacts.filter((artifact) => artifact.sessionId === sessionId && (!tier || artifact.tier === tier));
|
|
193
|
+
const recordsCap = tier ? tierMaxRecords[tier] : maxRecords;
|
|
194
|
+
const bytesCap = tier ? tierMaxBytes[tier] : maxBytes;
|
|
195
|
+
return sessionArtifacts.length <= recordsCap && sessionArtifacts.reduce((sum, artifact) => sum + artifact.bytes, 0) <= bytesCap;
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
function prune(now = Date.now(), protectedIds = new Set<string>()): ContextBrokerStatus {
|
|
145
199
|
dropExpired(now, protectedIds);
|
|
146
200
|
|
|
147
201
|
for (const sessionId of new Set(artifacts.map((artifact) => artifact.sessionId))) {
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
for (const tier of ["cold", "warm", "hot"] as ContextArtifactTier[]) {
|
|
203
|
+
while (!withinCaps(sessionId, tier)) {
|
|
204
|
+
const candidate = removalCandidates(sessionId, protectedIds, tier)[0];
|
|
205
|
+
if (!candidate) break;
|
|
206
|
+
artifacts.splice(candidate.index, 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
while (!withinCaps(sessionId)) {
|
|
211
|
+
const candidate = removalCandidates(sessionId, protectedIds)[0];
|
|
150
212
|
if (!candidate) break;
|
|
151
213
|
artifacts.splice(candidate.index, 1);
|
|
152
214
|
}
|
|
@@ -169,10 +231,13 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
169
231
|
const id = `ctx-${now.toString(36)}-${String(artifactSequence).padStart(4, "0")}-${sha256.slice(0, 12)}`;
|
|
170
232
|
const session = safeName(input.sessionId || "session");
|
|
171
233
|
const kind = input.kind;
|
|
234
|
+
const tags = normalizeList(input.tags);
|
|
235
|
+
const baseTier = classifyBaseTier(input, tags);
|
|
236
|
+
const tier: ContextArtifactTier = input.pinned ? "hot" : baseTier;
|
|
172
237
|
const handle = `ctx://session/${session}/${kind}/${sha256.slice(0, 16)}/${id}`;
|
|
173
|
-
const ttlMs = input.ttlMs ??
|
|
238
|
+
const ttlMs = input.ttlMs ?? tierTtlMs[tier];
|
|
174
239
|
|
|
175
|
-
const artifact: ContextArtifact & { sequence: number } = {
|
|
240
|
+
const artifact: ContextArtifact & { sequence: number; baseTier: ContextArtifactTier } = {
|
|
176
241
|
id,
|
|
177
242
|
handle,
|
|
178
243
|
sessionId: input.sessionId,
|
|
@@ -183,14 +248,16 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
183
248
|
sha256,
|
|
184
249
|
payload,
|
|
185
250
|
summary: summarizeArtifact(input.summary, kind, bytes, sha256, summaryBytes),
|
|
186
|
-
tags
|
|
251
|
+
tags,
|
|
187
252
|
paths: normalizeList(input.paths),
|
|
188
253
|
command: input.command?.trim() || undefined,
|
|
189
254
|
branch: input.branch?.trim() || undefined,
|
|
255
|
+
tier,
|
|
190
256
|
expiresAt: ttlMs > 0 ? now + ttlMs : undefined,
|
|
191
257
|
pinned: Boolean(input.pinned),
|
|
192
258
|
parentIds: normalizeList(input.parentIds),
|
|
193
259
|
sequence: artifactSequence,
|
|
260
|
+
baseTier,
|
|
194
261
|
};
|
|
195
262
|
|
|
196
263
|
artifacts = [artifact, ...artifacts];
|
|
@@ -203,7 +270,10 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
203
270
|
const limit = Math.max(1, Math.floor(query.limit ?? (artifacts.length || 1)));
|
|
204
271
|
return artifacts
|
|
205
272
|
.filter((artifact) => artifactMatches(artifact, query))
|
|
206
|
-
.sort((a, b) => Number(b.pinned) - Number(a.pinned)
|
|
273
|
+
.sort((a, b) => Number(b.pinned) - Number(a.pinned)
|
|
274
|
+
|| TIER_ORDER[a.tier] - TIER_ORDER[b.tier]
|
|
275
|
+
|| b.createdAt - a.createdAt
|
|
276
|
+
|| b.sequence - a.sequence)
|
|
207
277
|
.slice(0, limit);
|
|
208
278
|
}
|
|
209
279
|
|
|
@@ -212,6 +282,7 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
212
282
|
const artifact = artifacts.find((candidate) => candidate.id === idOrHandle || candidate.handle === idOrHandle) ?? null;
|
|
213
283
|
if (!artifact) return null;
|
|
214
284
|
artifact.pinned = pinned;
|
|
285
|
+
artifact.tier = pinned ? "hot" : artifact.baseTier;
|
|
215
286
|
artifact.updatedAt = Date.now();
|
|
216
287
|
prune();
|
|
217
288
|
return artifacts.find((candidate) => candidate.id === artifact.id) ?? null;
|
|
@@ -219,17 +290,25 @@ export function createInMemoryContextBroker(options: ContextBrokerOptions = {}):
|
|
|
219
290
|
|
|
220
291
|
function renderBrief(query: ContextLookupQuery & { budgetBytes?: number } = {}): string {
|
|
221
292
|
const budget = Math.max(64, Math.floor(query.budgetBytes ?? defaultBriefBytes));
|
|
293
|
+
const explicitCold = query.tier === "cold" || Boolean(query.handle || query.id);
|
|
294
|
+
const baseQuery = { ...query };
|
|
295
|
+
delete (baseQuery as { budgetBytes?: number }).budgetBytes;
|
|
296
|
+
const candidates = lookup({ ...baseQuery, limit: query.limit ?? 32 })
|
|
297
|
+
.filter((artifact) => explicitCold || artifact.tier !== "cold");
|
|
298
|
+
const hot = candidates.filter((artifact) => artifact.tier === "hot");
|
|
299
|
+
const warm = candidates.filter((artifact) => artifact.tier === "warm");
|
|
300
|
+
const cold = candidates.filter((artifact) => artifact.tier === "cold");
|
|
222
301
|
const lines = [
|
|
223
302
|
"## Context Broker",
|
|
224
303
|
`Budget: ${budget} bytes`,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
304
|
+
hot.length ? "Hot:" : "",
|
|
305
|
+
...hot.map(tierLine),
|
|
306
|
+
warm.length ? "Warm:" : "",
|
|
307
|
+
...warm.map(tierLine),
|
|
308
|
+
cold.length ? "Cold:" : "",
|
|
309
|
+
...cold.map(tierLine),
|
|
231
310
|
"Lookup: use broker lookup by handle/path/tag/kind/session before replaying raw payloads.",
|
|
232
|
-
];
|
|
311
|
+
].filter(Boolean);
|
|
233
312
|
|
|
234
313
|
return truncateUtf8(lines.join("\n"), budget);
|
|
235
314
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { createSqliteContextBroker } from "./sqlite.js";
|
|
6
|
+
|
|
7
|
+
describe("createSqliteContextBroker", () => {
|
|
8
|
+
it("persists handles, payloads, tiers, and pin state without replay reconstruction", () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
10
|
+
try {
|
|
11
|
+
const path = join(dir, "artifacts.sqlite");
|
|
12
|
+
let broker = createSqliteContextBroker({ path, defaultTtlMs: 0, briefBytes: 800 });
|
|
13
|
+
const warm = broker.publish({ sessionId: "s", kind: "tool_output", payload: "needle payload", summary: "warm summary", createdAt: Date.now() });
|
|
14
|
+
const cold = broker.publish({ sessionId: "s", kind: "subagent_result", payload: "archived payload", summary: "cold archive", tier: "cold", createdAt: Date.now() + 1 });
|
|
15
|
+
expect(broker.pin(cold.handle, true)?.tier).toBe("hot");
|
|
16
|
+
|
|
17
|
+
broker = createSqliteContextBroker({ path, defaultTtlMs: 0, briefBytes: 800 });
|
|
18
|
+
|
|
19
|
+
expect(broker.lookup({ handle: warm.handle })[0]?.payload).toBe("needle payload");
|
|
20
|
+
const reloadedCold = broker.lookup({ handle: cold.handle })[0];
|
|
21
|
+
expect(reloadedCold?.pinned).toBe(true);
|
|
22
|
+
expect(reloadedCold?.tier).toBe("hot");
|
|
23
|
+
expect(broker.renderBrief({ sessionId: "s" })).toContain(cold.handle);
|
|
24
|
+
} finally {
|
|
25
|
+
rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("uses SQLite FTS for text lookup and enforces tier caps", () => {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
31
|
+
try {
|
|
32
|
+
const broker = createSqliteContextBroker({ path: join(dir, "artifacts.sqlite"), defaultTtlMs: 0, coldMaxRecords: 1 });
|
|
33
|
+
const firstCold = broker.publish({ sessionId: "s", kind: "tool_output", payload: "alpha archive", tier: "cold", createdAt: Date.now() });
|
|
34
|
+
const secondCold = broker.publish({ sessionId: "s", kind: "tool_output", payload: "needle beta archive", tier: "cold", createdAt: Date.now() + 1 });
|
|
35
|
+
|
|
36
|
+
expect(broker.lookup({ id: firstCold.id })).toEqual([]);
|
|
37
|
+
expect(broker.lookup({ text: "needle" })[0]?.handle).toBe(secondCold.handle);
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("dedupes replayed source artifacts so durable handles survive caps", () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
45
|
+
try {
|
|
46
|
+
const path = join(dir, "artifacts.sqlite");
|
|
47
|
+
let broker = createSqliteContextBroker({ path, defaultTtlMs: 0, maxRecords: 1 });
|
|
48
|
+
const original = broker.publish({ sessionId: "s", kind: "tool_output", payload: "same replayed payload", parentIds: ["tool-call-1"], createdAt: Date.now() });
|
|
49
|
+
|
|
50
|
+
broker = createSqliteContextBroker({ path, defaultTtlMs: 0, maxRecords: 1 });
|
|
51
|
+
const replayed = broker.publish({ sessionId: "s", kind: "tool_output", payload: "same replayed payload", parentIds: ["tool-call-1"], createdAt: Date.now() + 1 });
|
|
52
|
+
|
|
53
|
+
expect(replayed.handle).toBe(original.handle);
|
|
54
|
+
expect(broker.lookup({ handle: original.handle })[0]?.payload).toBe("same replayed payload");
|
|
55
|
+
expect(broker.status().records).toBe(1);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("republishes expired replayed sources instead of returning dead handles", () => {
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), "ctx-sqlite-test-"));
|
|
63
|
+
try {
|
|
64
|
+
const path = join(dir, "artifacts.sqlite");
|
|
65
|
+
let broker = createSqliteContextBroker({ path, defaultTtlMs: 1 });
|
|
66
|
+
const expired = broker.publish({ sessionId: "s", kind: "tool_output", payload: "expired payload", parentIds: ["tool-call-1"], createdAt: 1 });
|
|
67
|
+
expect(broker.lookup({ handle: expired.handle })).toEqual([]);
|
|
68
|
+
|
|
69
|
+
broker = createSqliteContextBroker({ path, defaultTtlMs: 1 });
|
|
70
|
+
const replayed = broker.publish({ sessionId: "s", kind: "tool_output", payload: "fresh replayed payload", parentIds: ["tool-call-1"], createdAt: 1, ttlMs: Date.now() + 60_000 });
|
|
71
|
+
|
|
72
|
+
expect(replayed.handle).not.toBe(expired.handle);
|
|
73
|
+
expect(broker.lookup({ handle: replayed.handle })[0]?.payload).toBe("fresh replayed payload");
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|