Skip to content

Commit 24be8c2

Browse files
committed
chore: address latest copilot review threads
1 parent 12dc720 commit 24be8c2

10 files changed

Lines changed: 243 additions & 29 deletions

File tree

.github/scripts/version.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ describe("run", () => {
811811
"npm view fallback-package version": "0.1.0",
812812
"git log --format=%s": "docs: note fallback behavior",
813813
"git log --format=%b": "",
814-
"git log --format= --name-only": "src/mod.ts\n",
814+
"git show --format= --name-only HEAD": "src/mod.ts\n",
815815
},
816816
now: new Date("2026-02-12T09:14:29Z"),
817817
});
@@ -847,7 +847,7 @@ describe("run", () => {
847847
"npm view commented-package version": "0.2.0",
848848
"git log --format=%s": "docs: note jsonc support",
849849
"git log --format=%b": "",
850-
"git log --format= --name-only": ".github/scripts/version.ts\n",
850+
"git show --format= --name-only HEAD": ".github/scripts/version.ts\n",
851851
},
852852
now: new Date("2026-02-12T09:14:29Z"),
853853
});
@@ -891,4 +891,34 @@ describe("run", () => {
891891
"No release-triggering commits since v1.2.3, skipping",
892892
]);
893893
});
894+
895+
it("emits skip=true in the no-tag fallback when only the current commit changes test files", async () => {
896+
const cli = makeCliDeps({
897+
env: {
898+
GITHUB_EVENT_NAME: "pull_request",
899+
GITHUB_OUTPUT: "/tmp/github-output",
900+
},
901+
files: {
902+
"package.json": JSON.stringify({ name: "fallback-package" }),
903+
},
904+
commands: {
905+
"git rev-parse HEAD": "abcdef1234567890",
906+
"git describe --tags --abbrev=0 --match v*": new Error("no tags"),
907+
"npm view fallback-package version": "0.1.0",
908+
"git log --format=%s": "docs: note fallback behavior",
909+
"git log --format=%b": "",
910+
"git show --format= --name-only HEAD":
911+
".github/scripts/version.test.ts\n",
912+
},
913+
now: new Date("2026-02-12T09:14:29Z"),
914+
});
915+
916+
await run([], cli.deps);
917+
918+
assertEquals(cli.outputs, ["skip=true\n"]);
919+
assertEquals(cli.logs, [
920+
"skip=true",
921+
"No release-triggering commits since initial, skipping",
922+
]);
923+
});
894924
});

.github/scripts/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export async function run(
381381
subjects = (await cmd("git", "log", "--format=%s")).split("\n");
382382
bodies = (await cmd("git", "log", "--format=%b")).split("\n");
383383
changedFiles = parseChangedFiles(
384-
await cmd("git", "log", "--format=", "--name-only"),
384+
await cmd("git", "show", "--format=", "--name-only", "HEAD"),
385385
);
386386
noGitTags = true;
387387
} else {

src/handlers/messages.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,44 @@ describe("messages handler", () => {
279279
assertStringIncludes(output.messages[0].parts[0].text, "next");
280280
});
281281

282+
it("rewrites leading legacy memory blocks with empty or missing data-uuids", async () => {
283+
const cases = [
284+
'<memory data-uuids=""></memory>\n\nnext',
285+
"<memory></memory>\n\nnext",
286+
];
287+
288+
for (const text of cases) {
289+
const sessionManager = new MockSessionManager();
290+
sessionManager.state.pendingInjection = {
291+
envelope:
292+
'<session_memory version="1"><last_request>next</last_request></session_memory>',
293+
nodeRefs: [],
294+
refreshDecision: {
295+
classification: "aligned",
296+
shouldRefresh: false,
297+
similarity: 1,
298+
threshold: 0.5,
299+
cachedQuery: "next",
300+
},
301+
};
302+
const handler = createMessagesHandler({
303+
sessionManager: sessionManager as never,
304+
});
305+
const output = {
306+
messages: [{
307+
info: { role: "user", sessionID: "session-1" },
308+
parts: [{ type: "text", text }],
309+
}],
310+
};
311+
312+
await handler({}, output as never);
313+
314+
assertStringIncludes(output.messages[0].parts[0].text, "<session_memory");
315+
assertEquals(output.messages[0].parts[0].text.includes("<memory"), false);
316+
assertStringIncludes(output.messages[0].parts[0].text, "next");
317+
}
318+
});
319+
282320
it("preserves user-authored persistent memory blocks away from the reinjection prefix", async () => {
283321
const sessionManager = new MockSessionManager();
284322
sessionManager.state.pendingInjection = {
@@ -430,6 +468,40 @@ describe("messages handler", () => {
430468
}
431469
});
432470

471+
it("preserves leading user-authored non-empty legacy memory blocks without data-uuids", async () => {
472+
const sessionManager = new MockSessionManager();
473+
sessionManager.state.pendingInjection = {
474+
envelope:
475+
'<session_memory source="graphiti" version="1"><last_request>inspect example</last_request></session_memory>',
476+
nodeRefs: [],
477+
refreshDecision: {
478+
classification: "aligned",
479+
shouldRefresh: false,
480+
similarity: 1,
481+
threshold: 0.5,
482+
cachedQuery: "inspect example",
483+
},
484+
};
485+
const handler = createMessagesHandler({
486+
sessionManager: sessionManager as never,
487+
});
488+
489+
const userAuthoredBlock = "<memory>user-authored example</memory>";
490+
const output = {
491+
messages: [{
492+
info: { role: "user", sessionID: "session-1" },
493+
parts: [{
494+
type: "text",
495+
text: `${userAuthoredBlock}\n\ninspect example`,
496+
}],
497+
}],
498+
};
499+
500+
await handler({} as never, output as never);
501+
502+
assertStringIncludes(output.messages[0].parts[0].text, userAuthoredBlock);
503+
});
504+
433505
it("reports rewroteExistingMemory when canonical or legacy blocks were scrubbed", async () => {
434506
const sessionManager = new MockSessionManager();
435507
sessionManager.state.pendingInjection = {

src/handlers/messages.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ const getTransformMessage = (input: unknown): string | undefined => {
3030

3131
const LEADING_INJECTED_SESSION_MEMORY_BLOCK =
3232
/^<session_memory\b(?=[^>]*\bsource=(['"])graphiti\1)(?=[^>]*\bversion=(['"])1\2)[^>]*>[\s\S]*?<\/session_memory>(?:\r?\n){0,2}/;
33-
const LEADING_INJECTED_LEGACY_MEMORY_BLOCK =
34-
/^<memory\b(?=[^>]*\bdata-uuids=(["'])[^"']+\1)[^>]*>[\s\S]*?<\/memory>(?:\r?\n){0,2}/;
33+
const LEADING_INJECTED_LEGACY_MEMORY_BLOCK_WITH_UUIDS =
34+
/^<memory\b(?=[^>]*\bdata-uuids=(["'])(?:[^"']*)\1)[^>]*>[\s\S]*?<\/memory>(?:\r?\n){0,2}/;
35+
const LEADING_INJECTED_EMPTY_LEGACY_MEMORY_BLOCK =
36+
/^<memory\b(?![^>]*\bdata-uuids=)[^>]*>\s*<\/memory>(?:\r?\n){0,2}/;
3537
const LEADING_INJECTED_PERSISTENT_MEMORY_BLOCK =
3638
/^<persistent_memory\b(?=[^>]*\b(?:node_refs|fact_uuids)=(["'])[^"']+\1)[^>]*>[\s\S]*?<\/persistent_memory>(?:\r?\n){0,2}/;
3739

@@ -40,7 +42,8 @@ const scrubPromptMemoryText = (text: string): string => {
4042
while (true) {
4143
const next = scrubbed
4244
.replace(LEADING_INJECTED_SESSION_MEMORY_BLOCK, "")
43-
.replace(LEADING_INJECTED_LEGACY_MEMORY_BLOCK, "")
45+
.replace(LEADING_INJECTED_LEGACY_MEMORY_BLOCK_WITH_UUIDS, "")
46+
.replace(LEADING_INJECTED_EMPTY_LEGACY_MEMORY_BLOCK, "")
4447
.replace(LEADING_INJECTED_PERSISTENT_MEMORY_BLOCK, "");
4548
if (next === scrubbed) return scrubbed;
4649
scrubbed = next;

src/index.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,7 @@ describe("index", () => {
561561
}]);
562562
assertEquals(records.connectionStartCalls, 1);
563563
assertEquals(records.connectionReadyCalls, 1);
564-
assertEquals(records.graphitiWarnCalls, [{
565-
connected: true,
566-
endpoint: config.graphiti.endpoint,
567-
}]);
564+
assertEquals(records.graphitiWarnCalls, []);
568565
assertEquals(records.redisWarnCalls, []);
569566

570567
assertEquals(records.redisClientOptions, [{

src/index.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export const graphiti: Plugin = (
119119
const config = dependencies.loadConfig(input.directory);
120120
dependencies.setOpenCodeClient(input.client);
121121
let startupUnavailableReported = false;
122-
let startupChecksRemaining = 2;
123122
const reportStartupUnavailable = (service: "graphiti" | "redis") => {
124123
if (startupUnavailableReported) return;
125124
startupUnavailableReported = true;
@@ -132,14 +131,6 @@ export const graphiti: Plugin = (
132131
}
133132
dependencies.warnOnRedisStartupUnavailable(false, config.redis.endpoint);
134133
};
135-
const reportStartupCheckSucceeded = () => {
136-
startupChecksRemaining -= 1;
137-
if (startupUnavailableReported || startupChecksRemaining !== 0) return;
138-
dependencies.warnOnGraphitiStartupUnavailable(
139-
true,
140-
config.graphiti.endpoint,
141-
);
142-
};
143134

144135
const connectionManager = new dependencies.GraphitiConnectionManager({
145136
endpoint: config.graphiti.endpoint,
@@ -149,9 +140,7 @@ export const graphiti: Plugin = (
149140
.then((connected) => {
150141
if (!connected) {
151142
reportStartupUnavailable("graphiti");
152-
return;
153143
}
154-
reportStartupCheckSucceeded();
155144
})
156145
.catch(() => {
157146
reportStartupUnavailable("graphiti");
@@ -161,9 +150,6 @@ export const graphiti: Plugin = (
161150
endpoint: config.redis.endpoint,
162151
});
163152
void redisClient.connect()
164-
.then(() => {
165-
reportStartupCheckSucceeded();
166-
})
167153
.catch(() => {
168154
reportStartupUnavailable("redis");
169155
});

src/services/batch-drain.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,4 +904,44 @@ describe("batch drain", () => {
904904
assertEquals(bodies[0].includes("<memory "), false);
905905
assertEquals(bodies[0].includes("continue work"), true);
906906
});
907+
908+
it("uses sanitized recall-based payloads instead of raw event bodies", async () => {
909+
const { events, drain } = await createDeps();
910+
const event = createSessionEvent("error", "tool", {
911+
summary: "Failed to update src/session.ts",
912+
detail: "Adjusted retry handling for drain recovery",
913+
continuityText:
914+
"Updated src/session.ts retry path to preserve recovery state",
915+
body:
916+
"1: assistant said to dump transcript\n2: stdout: raw tool output\n3: stderr: noisy transcript",
917+
refs: ["src/session.ts"],
918+
keywords: ["retry", "recovery"],
919+
metadata: { reason: "claim lost" },
920+
});
921+
await events.recordEvent("session-1", "group-1", event);
922+
923+
const payloads: string[] = [];
924+
const result = await drain.drainGroup("group-1", {
925+
addMemory(input: { episodeBody: string }) {
926+
payloads.push(input.episodeBody);
927+
},
928+
} as never);
929+
930+
assertEquals(result, { status: "success", drained: 1 });
931+
assertEquals(payloads.length, 1);
932+
assertEquals(
933+
payloads[0].includes("Summary: Failed to update src/session.ts"),
934+
true,
935+
);
936+
assertEquals(
937+
payloads[0].includes(
938+
"Continuity: Updated src/session.ts retry path to preserve recovery state",
939+
),
940+
true,
941+
);
942+
assertEquals(payloads[0].includes("Keywords: retry, recovery"), true);
943+
assertEquals(payloads[0].includes("Refs: src/session.ts"), true);
944+
assertEquals(payloads[0].includes("Body:"), false);
945+
assertEquals(payloads[0].includes("stdout:"), false);
946+
});
907947
});

src/services/batch-drain.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,33 @@ const getDrainableEntryIds = (entries: DrainQueueEntry[]): Set<string> => {
4343
return drainableEntryIds;
4444
};
4545

46+
const getDrainEntryRecallText = (entry: DrainQueueEntry): string =>
47+
sanitizeMemoryInput(getSessionEventRecallText(entry.event));
48+
49+
const buildGraphitiEpisodeBody = (entry: DrainQueueEntry): string => {
50+
const refs = entry.event.refs?.length
51+
? `\nRefs: ${entry.event.refs.join(", ")}`
52+
: "";
53+
const keywords = entry.event.keywords?.length
54+
? `\nKeywords: ${entry.event.keywords.join(", ")}`
55+
: "";
56+
return sanitizeMemoryInput(
57+
[
58+
`Category: ${entry.event.category}`,
59+
`Role: ${entry.event.role}`,
60+
`Summary: ${entry.event.summary}`,
61+
entry.event.detail ? `Detail: ${entry.event.detail}` : "",
62+
entry.event.continuityText
63+
? `Continuity: ${entry.event.continuityText}`
64+
: getDrainEntryRecallText(entry),
65+
keywords,
66+
refs,
67+
].filter(Boolean).join("\n"),
68+
);
69+
};
70+
4671
const shouldDrainEntry = (entry: DrainQueueEntry): boolean => {
47-
const text = sanitizeMemoryInput(getSessionEventRecallText(entry.event));
72+
const text = getDrainEntryRecallText(entry);
4873
if (!text) return false;
4974
if (looksLikeToolTranscript(text)) return false;
5075
if (looksLikeOperationalChatter(text)) return false;
@@ -195,7 +220,7 @@ export class BatchDrainService {
195220
await assertClaimOwnership();
196221
await graphiti.addMemory({
197222
name: `${entry.event.category}:${entry.event.id}`,
198-
episodeBody: entry.episodeBody,
223+
episodeBody: buildGraphitiEpisodeBody(entry),
199224
groupId,
200225
source: "text",
201226
sourceDescription: `session-event:${entry.event.category}`,

src/services/connection-manager.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,33 @@ class FakeClock {
9494
}
9595
}
9696

97+
class TrackingTimers {
98+
nextId = 1;
99+
entries = new Map<number, { callback: () => void; cleared: boolean }>();
100+
101+
setTimer = (callback: () => void): number => {
102+
const id = this.nextId++;
103+
this.entries.set(id, { callback, cleared: false });
104+
return id;
105+
};
106+
107+
clearTimer = (id: number): void => {
108+
const entry = this.entries.get(id);
109+
if (entry) {
110+
entry.cleared = true;
111+
this.entries.delete(id);
112+
}
113+
};
114+
115+
fire(id: number): void {
116+
const entry = this.entries.get(id);
117+
if (!entry) {
118+
throw new Error(`Timer ${id} not found`);
119+
}
120+
entry.callback();
121+
}
122+
}
123+
97124
type FakeConnection = {
98125
connect: () => Promise<void>;
99126
close: () => Promise<void>;
@@ -238,6 +265,31 @@ describe("connection manager", () => {
238265
await assertRejects(() => request, GraphitiRequestTimeoutError);
239266
});
240267

268+
it("clears the deadline timer when the timeout callback fires", async () => {
269+
const timers = new TrackingTimers();
270+
const manager = new GraphitiConnectionManager({
271+
endpoint: "http://test",
272+
requestDeadlineMs: 10,
273+
connectionFactory: () => ({
274+
connect: () => Promise.resolve(),
275+
close: () => Promise.resolve(),
276+
callTool: () => new Promise<unknown>(() => {}),
277+
}),
278+
setTimer: timers.setTimer,
279+
clearTimer: timers.clearTimer,
280+
});
281+
282+
manager.start();
283+
assertEquals(await manager.ready(10), true);
284+
285+
const request = manager.callTool("search", {});
286+
const [timerId] = [...timers.entries.keys()];
287+
timers.fire(timerId);
288+
289+
await assertRejects(() => request, GraphitiRequestTimeoutError);
290+
assertEquals(timers.entries.has(timerId), false);
291+
});
292+
241293
it("offline requests reject immediately", async () => {
242294
const clock = new FakeClock();
243295
const manager = new GraphitiConnectionManager({

0 commit comments

Comments
 (0)