Skip to content

Commit 877a41e

Browse files
committed
fix(plugin): inject graphiti memory into user message
1 parent 8ad4f53 commit 877a41e

9 files changed

Lines changed: 173 additions & 39 deletions

File tree

dnt.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ await build({
4545
types: "./esm/mod.d.ts",
4646
opencode: {
4747
type: "plugin",
48-
hooks: ["chat.message", "event", "experimental.session.compacting"],
48+
hooks: [
49+
"chat.message",
50+
"event",
51+
"experimental.session.compacting",
52+
"experimental.chat.messages.transform",
53+
],
4954
},
5055
},
5156
});

src/handlers/chat.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export function createChatHandler(deps: ChatHandlerDeps) {
3232
contextLimit: number;
3333
lastInjectionFactUuids: string[];
3434
cachedMemoryContext?: string;
35+
cachedFactUuids?: string[];
36+
visibleFactUuids?: string[];
3537
},
3638
messageText: string,
3739
useUserScope: boolean,
@@ -80,6 +82,36 @@ export function createChatHandler(deps: ChatHandlerDeps) {
8082
facts: userFacts,
8183
nodes: userNodes,
8284
});
85+
86+
const visibleSet = new Set(state.visibleFactUuids ?? []);
87+
const beforeProjectFacts = projectContext.facts.length;
88+
const beforeUserFacts = userContext.facts.length;
89+
projectContext.facts = projectContext.facts.filter((fact) =>
90+
!visibleSet.has(fact.uuid)
91+
);
92+
userContext.facts = userContext.facts.filter((fact) =>
93+
!visibleSet.has(fact.uuid)
94+
);
95+
logger.debug("Filtered visible facts from injection", {
96+
visibleCount: visibleSet.size,
97+
filteredProjectFacts: beforeProjectFacts - projectContext.facts.length,
98+
filteredUserFacts: beforeUserFacts - userContext.facts.length,
99+
remainingProjectFacts: projectContext.facts.length,
100+
remainingUserFacts: userContext.facts.length,
101+
});
102+
103+
if (
104+
projectContext.facts.length === 0 &&
105+
userContext.facts.length === 0 &&
106+
projectContext.nodes.length === 0 &&
107+
userContext.nodes.length === 0
108+
) {
109+
logger.debug("All facts filtered; skipping context cache", {
110+
groupId: state.groupId,
111+
userGroupId: state.userGroupId,
112+
});
113+
return;
114+
}
83115
const projectContextString = formatMemoryContext(
84116
projectContext.facts,
85117
projectContext.nodes,
@@ -144,10 +176,11 @@ export function createChatHandler(deps: ChatHandlerDeps) {
144176
];
145177
const factUuids = seedFactUuids ?? Array.from(new Set(allFactUuids));
146178
state.cachedMemoryContext = memoryContext;
179+
state.cachedFactUuids = factUuids;
147180
logger.info(
148181
`Cached ${projectFacts.length + userFacts.length} facts and ${
149182
projectNodes.length + userNodes.length
150-
} nodes for system prompt injection`,
183+
} nodes for user message injection`,
151184
);
152185
state.lastInjectionFactUuids = factUuids;
153186
};

src/handlers/event.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export function createEventHandler(deps: EventHandlerDeps) {
9292
userGroupId: defaultUserGroupId,
9393
injectedMemories: false,
9494
lastInjectionFactUuids: [],
95+
cachedMemoryContext: undefined,
96+
cachedFactUuids: undefined,
97+
visibleFactUuids: [],
9598
messageCount: 0,
9699
pendingMessages: [],
97100
contextLimit: 200_000,

src/handlers/messages.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Hooks } from "@opencode-ai/plugin";
2+
import { extractVisibleUuids } from "../services/context.ts";
3+
import { logger } from "../services/logger.ts";
4+
import type { SessionManager } from "../session.ts";
5+
6+
type MessagesTransformHook = NonNullable<
7+
Hooks["experimental.chat.messages.transform"]
8+
>;
9+
type MessagesTransformInput = Parameters<MessagesTransformHook>[0];
10+
type MessagesTransformOutput = Parameters<MessagesTransformHook>[1];
11+
12+
export interface MessagesHandlerDeps {
13+
sessionManager: SessionManager;
14+
}
15+
16+
export function createMessagesHandler(deps: MessagesHandlerDeps) {
17+
const { sessionManager } = deps;
18+
19+
// deno-lint-ignore require-await
20+
return async (
21+
_input: MessagesTransformInput,
22+
output: MessagesTransformOutput,
23+
) => {
24+
const lastUserEntry = [...output.messages]
25+
.reverse()
26+
.find((message) => message.info.role === "user");
27+
if (!lastUserEntry) return;
28+
29+
const sessionID = lastUserEntry.info.sessionID;
30+
const state = sessionManager.getState(sessionID);
31+
if (!state?.isMain) {
32+
logger.debug("Skipping memory injection; not main session", {
33+
sessionID,
34+
});
35+
return;
36+
}
37+
38+
const allVisibleUuids: string[] = [];
39+
for (const entry of output.messages) {
40+
for (const part of entry.parts) {
41+
if (part.type === "text" && "text" in part) {
42+
const uuids = extractVisibleUuids((part as { text: string }).text);
43+
if (uuids.length > 0) {
44+
logger.debug("Found <memory> block UUIDs", {
45+
sessionID,
46+
uuids,
47+
messageID: entry.info.id,
48+
});
49+
}
50+
allVisibleUuids.push(...uuids);
51+
}
52+
}
53+
}
54+
state.visibleFactUuids = [...new Set(allVisibleUuids)];
55+
logger.debug("Updated visibleFactUuids from message scan", {
56+
sessionID,
57+
visibleCount: state.visibleFactUuids.length,
58+
});
59+
60+
if (!state.cachedMemoryContext) {
61+
logger.debug("Skipping memory injection; no cached context", {
62+
sessionID,
63+
});
64+
return;
65+
}
66+
67+
const textPart = lastUserEntry.parts.find(
68+
(part): part is typeof part & { type: "text"; text: string } =>
69+
part.type === "text" && "text" in part,
70+
);
71+
if (!textPart) {
72+
logger.debug("Skipping memory injection; no text part", {
73+
sessionID,
74+
});
75+
return;
76+
}
77+
78+
if (textPart.text.includes("<memory")) {
79+
logger.debug("Skipping memory injection; already injected", {
80+
sessionID,
81+
});
82+
state.cachedMemoryContext = undefined;
83+
state.cachedFactUuids = undefined;
84+
return;
85+
}
86+
87+
const uuids = state.cachedFactUuids ?? [];
88+
const uuidAttr = uuids.length > 0 ? ` data-uuids="${uuids.join(",")}"` : "";
89+
const memoryBlock =
90+
`<memory${uuidAttr}>\n${state.cachedMemoryContext}\n</memory>`;
91+
92+
textPart.text = `${memoryBlock}\n\n${textPart.text}`;
93+
94+
logger.info("Injected memory context into last user message", {
95+
sessionID,
96+
factCount: uuids.length,
97+
blockLength: memoryBlock.length,
98+
preview: state.cachedMemoryContext.slice(0, 100),
99+
});
100+
101+
state.cachedMemoryContext = undefined;
102+
state.cachedFactUuids = undefined;
103+
};
104+
}

src/handlers/system.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ describe("index", () => {
102102
// 2. Tests for event handlers (session.created, session.compacted, session.idle, etc.)
103103
// 3. Tests for chat.message hook (memory injection, buffering)
104104
// 4. Tests for experimental.session.compacting hook
105+
// 5. Tests for experimental.chat.messages.transform hook
105106
//
106107
// These tests should be added after Phase 2 refactoring, when the plugin logic
107108
// is extracted into testable units. For now, the individual helper functions

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { loadConfig } from "./config.ts";
33
import { createChatHandler } from "./handlers/chat.ts";
44
import { createCompactingHandler } from "./handlers/compacting.ts";
55
import { createEventHandler } from "./handlers/event.ts";
6-
import { createSystemHandler } from "./handlers/system.ts";
6+
import { createMessagesHandler } from "./handlers/messages.ts";
77
import { GraphitiClient } from "./services/client.ts";
88
import { logger } from "./services/logger.ts";
99
import { SessionManager } from "./session.ts";
@@ -63,7 +63,7 @@ export const graphiti: Plugin = async (input: PluginInput) => {
6363
defaultGroupId,
6464
factStaleDays: config.factStaleDays,
6565
}),
66-
"experimental.chat.system.transform": createSystemHandler({
66+
"experimental.chat.messages.transform": createMessagesHandler({
6767
sessionManager,
6868
}),
6969
};

src/services/context.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,19 @@ export function formatMemoryContext(
179179

180180
return sections.join("\n");
181181
}
182+
183+
/**
184+
* Extract fact UUIDs from all <memory data-uuids="..."> blocks in a text string.
185+
*/
186+
export function extractVisibleUuids(text: string): string[] {
187+
const uuids: string[] = [];
188+
const regex = /<memory[^>]*\bdata-uuids="([^"]*)"[^>]*>/g;
189+
let match;
190+
while ((match = regex.exec(text)) !== null) {
191+
const raw = match[1];
192+
if (raw) {
193+
uuids.push(...raw.split(",").filter(Boolean));
194+
}
195+
}
196+
return uuids;
197+
}

src/session.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ export type SessionState = {
1919
injectedMemories: boolean;
2020
/** Fact UUIDs included in the last memory injection. */
2121
lastInjectionFactUuids: string[];
22-
/** Cached formatted memory context for system prompt injection. */
22+
/** Cached formatted memory context for user message injection. */
2323
cachedMemoryContext?: string;
24+
/** Fact UUIDs from cached context, for embedding in <memory> tag. */
25+
cachedFactUuids?: string[];
26+
/** Fact UUIDs currently visible in <memory> blocks across all messages. */
27+
visibleFactUuids: string[];
2428
/** Count of messages observed in this session. */
2529
messageCount: number;
2630
/** Buffered message strings awaiting flush. */
@@ -110,6 +114,8 @@ export class SessionManager {
110114
injectedMemories: false,
111115
lastInjectionFactUuids: [],
112116
cachedMemoryContext: undefined,
117+
cachedFactUuids: undefined,
118+
visibleFactUuids: [],
113119
messageCount: 0,
114120
pendingMessages: [],
115121
contextLimit: 200_000,

0 commit comments

Comments
 (0)