opencode-graphiti 0.1.0 → 0.1.1

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.
Files changed (72) hide show
  1. package/README.md +44 -27
  2. package/esm/src/config.d.ts +3 -0
  3. package/esm/src/config.d.ts.map +1 -1
  4. package/esm/src/config.js +21 -35
  5. package/esm/src/handlers/chat.d.ts +21 -0
  6. package/esm/src/handlers/chat.d.ts.map +1 -0
  7. package/esm/src/handlers/chat.js +127 -0
  8. package/esm/src/handlers/compacting.d.ts +15 -0
  9. package/esm/src/handlers/compacting.d.ts.map +1 -0
  10. package/esm/src/handlers/compacting.js +29 -0
  11. package/esm/src/handlers/event.d.ts +18 -0
  12. package/esm/src/handlers/event.d.ts.map +1 -0
  13. package/esm/src/handlers/event.js +132 -0
  14. package/esm/src/index.d.ts +4 -1
  15. package/esm/src/index.d.ts.map +1 -1
  16. package/esm/src/index.js +29 -419
  17. package/esm/src/services/client.d.ts +33 -5
  18. package/esm/src/services/client.d.ts.map +1 -1
  19. package/esm/src/services/client.js +42 -20
  20. package/esm/src/services/compaction.d.ts +18 -69
  21. package/esm/src/services/compaction.d.ts.map +1 -1
  22. package/esm/src/services/compaction.js +86 -112
  23. package/esm/src/services/context-limit.d.ts +14 -0
  24. package/esm/src/services/context-limit.d.ts.map +1 -0
  25. package/esm/src/services/context-limit.js +41 -0
  26. package/esm/src/services/context.d.ts +5 -0
  27. package/esm/src/services/context.d.ts.map +1 -1
  28. package/esm/src/services/context.js +19 -16
  29. package/esm/src/session.d.ts +90 -0
  30. package/esm/src/session.d.ts.map +1 -0
  31. package/esm/src/session.js +304 -0
  32. package/esm/src/types/index.d.ts +28 -9
  33. package/esm/src/types/index.d.ts.map +1 -1
  34. package/esm/src/utils.d.ts +21 -0
  35. package/esm/src/utils.d.ts.map +1 -0
  36. package/esm/src/utils.js +34 -0
  37. package/package.json +2 -1
  38. package/script/src/config.d.ts +3 -0
  39. package/script/src/config.d.ts.map +1 -1
  40. package/script/src/config.js +21 -35
  41. package/script/src/handlers/chat.d.ts +21 -0
  42. package/script/src/handlers/chat.d.ts.map +1 -0
  43. package/script/src/handlers/chat.js +130 -0
  44. package/script/src/handlers/compacting.d.ts +15 -0
  45. package/script/src/handlers/compacting.d.ts.map +1 -0
  46. package/script/src/handlers/compacting.js +32 -0
  47. package/script/src/handlers/event.d.ts +18 -0
  48. package/script/src/handlers/event.d.ts.map +1 -0
  49. package/script/src/handlers/event.js +135 -0
  50. package/script/src/index.d.ts +4 -1
  51. package/script/src/index.d.ts.map +1 -1
  52. package/script/src/index.js +31 -421
  53. package/script/src/services/client.d.ts +33 -5
  54. package/script/src/services/client.d.ts.map +1 -1
  55. package/script/src/services/client.js +42 -53
  56. package/script/src/services/compaction.d.ts +18 -69
  57. package/script/src/services/compaction.d.ts.map +1 -1
  58. package/script/src/services/compaction.js +86 -113
  59. package/script/src/services/context-limit.d.ts +14 -0
  60. package/script/src/services/context-limit.d.ts.map +1 -0
  61. package/script/src/services/context-limit.js +45 -0
  62. package/script/src/services/context.d.ts +5 -0
  63. package/script/src/services/context.d.ts.map +1 -1
  64. package/script/src/services/context.js +22 -16
  65. package/script/src/session.d.ts +90 -0
  66. package/script/src/session.d.ts.map +1 -0
  67. package/script/src/session.js +308 -0
  68. package/script/src/types/index.d.ts +28 -9
  69. package/script/src/types/index.d.ts.map +1 -1
  70. package/script/src/utils.d.ts +21 -0
  71. package/script/src/utils.d.ts.map +1 -0
  72. package/script/src/utils.js +44 -0
package/README.md CHANGED
@@ -23,9 +23,12 @@ reminded of recent project context — regardless of what survived the summary.
23
23
  This plugin connects to a Graphiti MCP server and:
24
24
 
25
25
  - Injects relevant memories into the first user message of each session
26
- - Detects user-triggered memory saves ("remember this", "keep in mind", etc.)
26
+ - Periodically re-injects project memories as the conversation progresses
27
+ - Buffers user and assistant messages, flushing them to Graphiti on idle or
28
+ before compaction
27
29
  - Preserves key facts during context compaction
28
- - Scopes memories per project using directory-based group IDs
30
+ - Saves compaction summaries as episodes so knowledge survives across boundaries
31
+ - Scopes memories per project (and per user) using directory-based group IDs
29
32
 
30
33
  ## Prerequisites
31
34
 
@@ -101,47 +104,61 @@ Create a config file at `~/.config/opencode/graphiti.jsonc`:
101
104
  // Prefix for project group IDs (e.g. "opencode_my-project")
102
105
  "groupIdPrefix": "opencode",
103
106
 
104
- // Maximum results to retrieve
105
- "maxFacts": 10,
106
- "maxNodes": 5,
107
- "maxEpisodes": 5,
108
-
109
- // Feature toggles
110
- "injectOnFirstMessage": true,
111
- "enableTriggerDetection": true,
112
- "enableCompactionSave": true
107
+ // Number of user messages between memory re-injections (0 = disabled)
108
+ "injectionInterval": 10
113
109
  }
114
110
  ```
115
111
 
116
- All fields are optional — defaults are used for any missing values.
112
+ All fields are optional — defaults (shown above) are used for any missing
113
+ values.
117
114
 
118
115
  ## How It Works
119
116
 
120
117
  ### Memory Injection (`chat.message`)
121
118
 
122
119
  On the first user message in a session, the plugin searches Graphiti for facts
123
- and entities relevant to the message content. Matching results are formatted and
124
- prepended to the conversation as a synthetic context block.
120
+ and entities relevant to the message content. Results are split into project and
121
+ user scopes (70% / 30% budget split), formatted, and prepended to the
122
+ conversation as a synthetic context block.
123
+
124
+ The injection budget is calculated dynamically: 5% of the model's context limit
125
+ (resolved from the provider list) multiplied by 4 characters per token.
126
+
127
+ ### Re-injection (`chat.message`)
128
+
129
+ When `injectionInterval` is greater than 0, the plugin periodically re-injects
130
+ project-scoped memories as the conversation progresses. After every N user
131
+ messages (where N is `injectionInterval`), stale synthetic memory parts are
132
+ replaced with fresh results. Re-injection uses the full character budget for
133
+ project memories only (no user scope).
125
134
 
126
- ### Trigger Detection (`chat.message`)
135
+ ### Message Buffering (`event`)
127
136
 
128
- User messages are scanned for phrases like:
137
+ User and assistant messages are buffered in memory as they arrive. The plugin
138
+ listens on `message.part.updated` to capture assistant text as it streams, and
139
+ on `message.updated` to finalize completed assistant replies. Buffered messages
140
+ are flushed to Graphiti as episodes:
129
141
 
130
- - "remember this", "memorize that"
131
- - "save this in memory", "keep this in mind"
132
- - "don't forget", "note this"
133
- - "for future reference"
142
+ - **On idle** (`session.idle`): when the session becomes idle with at least 50
143
+ bytes of buffered content.
144
+ - **Before compaction** (`session.compacted`): all buffered messages are flushed
145
+ immediately (no minimum size) so nothing is lost.
134
146
 
135
- When detected, the message content is saved as an episode in Graphiti.
147
+ If the last buffered message is from the user (i.e. no assistant reply was
148
+ captured), the plugin fetches the latest assistant message from the session API
149
+ as a fallback before flushing.
136
150
 
137
- ### Compaction Preservation (`event` + `experimental.session.compacting`)
151
+ ### Compaction Preservation (`session.compacted` + `experimental.session.compacting`)
138
152
 
139
- When OpenCode compacts the context window:
153
+ Compaction is handled entirely by OpenCode's native compaction mechanism. The
154
+ plugin participates in two ways:
140
155
 
141
- 1. **Before compaction**: The plugin injects known facts into the compaction
142
- context, so the summarizer preserves important knowledge.
143
- 2. **After compaction**: If a summary is produced, it is saved as an episode to
144
- Graphiti, ensuring knowledge survives across compaction boundaries.
156
+ 1. **Before compaction** (`experimental.session.compacting`): The plugin injects
157
+ known facts and entities into the compaction context using the same 70% / 30%
158
+ project/user budget split, so the summarizer preserves important knowledge.
159
+ 2. **After compaction** (`session.compacted`): The compaction summary is saved
160
+ as an episode to Graphiti, ensuring knowledge survives across compaction
161
+ boundaries.
145
162
 
146
163
  ### Project Scoping
147
164
 
@@ -1,3 +1,6 @@
1
1
  import type { GraphitiConfig } from "./types/index.js";
2
+ /**
3
+ * Load Graphiti configuration from JSONC files with defaults applied.
4
+ */
2
5
  export declare function loadConfig(): GraphitiConfig;
3
6
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/src/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA0BvD,wBAAgB,UAAU,IAAI,cAAc,CAe3C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAcvD;;GAEG;AACH,wBAAgB,UAAU,IAAI,cAAc,CAc3C"}
package/esm/src/config.js CHANGED
@@ -1,43 +1,29 @@
1
- import * as dntShim from "../_dnt.shims.js";
2
1
  import { cosmiconfigSync } from "cosmiconfig";
3
- import { readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import { join } from "node:path";
2
+ import * as z from "zod/mini";
6
3
  const DEFAULT_CONFIG = {
7
4
  endpoint: "http://localhost:8000/mcp",
8
5
  groupIdPrefix: "opencode",
9
- maxFacts: 10,
10
- maxNodes: 5,
11
- maxEpisodes: 5,
12
- injectOnFirstMessage: true,
13
- enableCompactionSave: true,
14
- compactionThreshold: 0.8,
15
- minTokensForCompaction: 50000,
16
- compactionCooldownMs: 30000,
17
- autoResumeAfterCompaction: true,
18
- };
19
- function parseJsonc(text) {
20
- let stripped = text.replace(/(^|\s)\/\/.*$/gm, "$1");
21
- stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, "");
22
- return JSON.parse(stripped);
23
- }
24
- const createExplorer = (cwd) => {
25
- return cosmiconfigSync("graphiti", { stopDir: cwd });
6
+ injectionInterval: 10,
26
7
  };
8
+ const GraphitiConfigSchema = z.object({
9
+ endpoint: z.string(),
10
+ groupIdPrefix: z.string(),
11
+ injectionInterval: z.number(),
12
+ });
13
+ /**
14
+ * Load Graphiti configuration from JSONC files with defaults applied.
15
+ */
27
16
  export function loadConfig() {
28
- const cwd = dntShim.Deno.cwd();
29
- const explorer = createExplorer(cwd);
30
- const result = explorer.search(cwd);
31
- let config = result?.config;
32
- if (!config) {
33
- const configPath = join(homedir(), ".config", "opencode", "graphiti.jsonc");
34
- try {
35
- const raw = readFileSync(configPath, "utf-8");
36
- config = parseJsonc(raw);
37
- }
38
- catch {
39
- config = null;
40
- }
17
+ const explorer = cosmiconfigSync("graphiti");
18
+ const result = explorer.search();
19
+ const candidate = result?.config ?? {};
20
+ const merged = {
21
+ ...DEFAULT_CONFIG,
22
+ ...candidate,
23
+ };
24
+ const parsed = GraphitiConfigSchema.safeParse(merged);
25
+ if (parsed.success) {
26
+ return parsed.data;
41
27
  }
42
- return { ...DEFAULT_CONFIG, ...(config ?? {}) };
28
+ return DEFAULT_CONFIG;
43
29
  }
@@ -0,0 +1,21 @@
1
+ import type { Part } from "@opencode-ai/sdk";
2
+ import type { GraphitiClient } from "../services/client.js";
3
+ import type { SessionManager } from "../session.js";
4
+ /** Dependencies for the chat message handler. */
5
+ export interface ChatHandlerDeps {
6
+ sessionManager: SessionManager;
7
+ injectionInterval: number;
8
+ client: GraphitiClient;
9
+ }
10
+ /** Creates the `chat.message` hook handler. */
11
+ export declare function createChatHandler(deps: ChatHandlerDeps): ({ sessionID }: {
12
+ sessionID: string;
13
+ }, output: {
14
+ allow_buffering?: boolean;
15
+ parts: Part[];
16
+ message: {
17
+ sessionID: string;
18
+ id: string;
19
+ };
20
+ }) => Promise<void>;
21
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/chat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,IA6FnD,eAAe;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACpC,QAAQ;IACN,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C,mBA4DJ"}
@@ -0,0 +1,127 @@
1
+ import { formatMemoryContext } from "../services/context.js";
2
+ import { calculateInjectionBudget } from "../services/context-limit.js";
3
+ import { logger } from "../services/logger.js";
4
+ import { extractTextFromParts } from "../utils.js";
5
+ /** Creates the `chat.message` hook handler. */
6
+ export function createChatHandler(deps) {
7
+ const { sessionManager, injectionInterval, client } = deps;
8
+ const removeSyntheticMemoryParts = (parts) => parts.filter((part) => {
9
+ const synthetic = part.synthetic;
10
+ const id = typeof part.id === "string"
11
+ ? String(part.id)
12
+ : "";
13
+ if (!synthetic)
14
+ return true;
15
+ if (id.startsWith("graphiti-memory-"))
16
+ return false;
17
+ if (id.startsWith("graphiti-refresh-"))
18
+ return false;
19
+ return true;
20
+ });
21
+ const injectMemoryContext = async (state, messageText, output, prefix, useUserScope, characterBudget) => {
22
+ const userGroupId = state.userGroupId;
23
+ const projectFactsPromise = client.searchFacts({
24
+ query: messageText,
25
+ groupIds: [state.groupId],
26
+ maxFacts: 50,
27
+ });
28
+ const projectNodesPromise = client.searchNodes({
29
+ query: messageText,
30
+ groupIds: [state.groupId],
31
+ maxNodes: 30,
32
+ });
33
+ const userFactsPromise = useUserScope && userGroupId
34
+ ? client.searchFacts({
35
+ query: messageText,
36
+ groupIds: [userGroupId],
37
+ maxFacts: 20,
38
+ })
39
+ : Promise.resolve([]);
40
+ const userNodesPromise = useUserScope && userGroupId
41
+ ? client.searchNodes({
42
+ query: messageText,
43
+ groupIds: [userGroupId],
44
+ maxNodes: 10,
45
+ })
46
+ : Promise.resolve([]);
47
+ const [projectFacts, projectNodes, userFacts, userNodes] = await Promise
48
+ .all([
49
+ projectFactsPromise,
50
+ projectNodesPromise,
51
+ userFactsPromise,
52
+ userNodesPromise,
53
+ ]);
54
+ const projectContext = formatMemoryContext(projectFacts, projectNodes);
55
+ const userContext = formatMemoryContext(userFacts, userNodes);
56
+ if (!projectContext && !userContext)
57
+ return;
58
+ const projectBudget = useUserScope
59
+ ? Math.floor(characterBudget * 0.7)
60
+ : characterBudget;
61
+ const userBudget = characterBudget - projectBudget;
62
+ const truncatedProject = projectContext.slice(0, projectBudget);
63
+ const truncatedUser = useUserScope ? userContext.slice(0, userBudget) : "";
64
+ const memoryContext = [truncatedProject, truncatedUser]
65
+ .filter((section) => section.trim().length > 0)
66
+ .join("\n\n")
67
+ .slice(0, characterBudget);
68
+ if (!memoryContext)
69
+ return;
70
+ output.parts.unshift({
71
+ type: "text",
72
+ text: memoryContext,
73
+ id: `${prefix}${Date.now()}`,
74
+ sessionID: output.message.sessionID,
75
+ messageID: output.message.id,
76
+ synthetic: true,
77
+ });
78
+ logger.info(`Injected ${projectFacts.length + userFacts.length} facts and ${projectNodes.length + userNodes.length} nodes`);
79
+ };
80
+ return async ({ sessionID }, output) => {
81
+ if (await sessionManager.isSubagentSession(sessionID)) {
82
+ logger.debug("Ignoring subagent chat message:", sessionID);
83
+ return;
84
+ }
85
+ const { state, resolved } = await sessionManager.resolveSessionState(sessionID);
86
+ if (!resolved) {
87
+ output.allow_buffering = true;
88
+ logger.debug("Unable to resolve session for message:", { sessionID });
89
+ return;
90
+ }
91
+ if (!state?.isMain) {
92
+ logger.debug("Ignoring subagent chat message:", sessionID);
93
+ return;
94
+ }
95
+ state.messageCount++;
96
+ const messageText = extractTextFromParts(output.parts);
97
+ if (!messageText)
98
+ return;
99
+ state.pendingMessages.push(`User: ${messageText}`);
100
+ logger.info("Buffered user message", {
101
+ hook: "chat.message",
102
+ sessionID,
103
+ messageLength: messageText.length,
104
+ });
105
+ const shouldInjectOnFirst = !state.injectedMemories;
106
+ const shouldReinject = !shouldInjectOnFirst &&
107
+ injectionInterval > 0 &&
108
+ (state.messageCount - state.lastInjectionMessageCount) >=
109
+ injectionInterval;
110
+ if (!shouldInjectOnFirst && !shouldReinject)
111
+ return;
112
+ if (shouldReinject) {
113
+ output.parts = removeSyntheticMemoryParts(output.parts);
114
+ }
115
+ try {
116
+ const prefix = shouldReinject ? "graphiti-refresh-" : "graphiti-memory-";
117
+ const useUserScope = shouldInjectOnFirst;
118
+ const characterBudget = calculateInjectionBudget(state.contextLimit);
119
+ await injectMemoryContext(state, messageText, output, prefix, useUserScope, characterBudget);
120
+ state.injectedMemories = true;
121
+ state.lastInjectionMessageCount = state.messageCount;
122
+ }
123
+ catch (err) {
124
+ logger.error("Failed to inject memories:", err);
125
+ }
126
+ };
127
+ }
@@ -0,0 +1,15 @@
1
+ import type { GraphitiClient } from "../services/client.js";
2
+ import type { SessionManager } from "../session.js";
3
+ /** Dependencies for the compacting handler. */
4
+ export interface CompactingHandlerDeps {
5
+ sessionManager: SessionManager;
6
+ client: GraphitiClient;
7
+ defaultGroupId: string;
8
+ }
9
+ /** Creates the `experimental.session.compacting` hook handler. */
10
+ export declare function createCompactingHandler(deps: CompactingHandlerDeps): ({ sessionID }: {
11
+ sessionID: string;
12
+ }, output: {
13
+ context: string[];
14
+ }) => Promise<void>;
15
+ //# sourceMappingURL=compacting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compacting.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/compacting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAI5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,kEAAkE;AAClE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,IAI/D,eAAe;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EACpC,QAAQ;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,mBAyBhC"}
@@ -0,0 +1,29 @@
1
+ import { getCompactionContext } from "../services/compaction.js";
2
+ import { calculateInjectionBudget } from "../services/context-limit.js";
3
+ import { logger } from "../services/logger.js";
4
+ /** Creates the `experimental.session.compacting` hook handler. */
5
+ export function createCompactingHandler(deps) {
6
+ const { sessionManager, client, defaultGroupId } = deps;
7
+ return async ({ sessionID }, output) => {
8
+ const state = sessionManager.getState(sessionID);
9
+ if (!state?.isMain) {
10
+ logger.debug("Ignoring non-main compaction context:", sessionID);
11
+ return;
12
+ }
13
+ const groupId = state.groupId || defaultGroupId;
14
+ const characterBudget = calculateInjectionBudget(state.contextLimit);
15
+ const additionalContext = await getCompactionContext({
16
+ client,
17
+ characterBudget,
18
+ groupIds: {
19
+ project: groupId,
20
+ user: state.userGroupId,
21
+ },
22
+ contextStrings: output.context,
23
+ });
24
+ if (additionalContext.length > 0) {
25
+ output.context.push(...additionalContext);
26
+ logger.info("Injected persistent knowledge into compaction context");
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,18 @@
1
+ import type { Event } from "@opencode-ai/sdk";
2
+ import type { GraphitiClient } from "../services/client.js";
3
+ import type { ProviderListClient } from "../services/context-limit.js";
4
+ import type { SessionManager } from "../session.js";
5
+ /** Dependencies for the event handler. */
6
+ export interface EventHandlerDeps {
7
+ sessionManager: SessionManager;
8
+ client: GraphitiClient;
9
+ defaultGroupId: string;
10
+ sdkClient: ProviderListClient;
11
+ directory: string;
12
+ groupIdPrefix: string;
13
+ }
14
+ /** Creates the `event` hook handler. */
15
+ export declare function createEventHandler(deps: EventHandlerDeps): ({ event }: {
16
+ event: Event;
17
+ }) => Promise<void>;
18
+ //# sourceMappingURL=event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../../src/src/handlers/event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,cAAc,CAAC;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,kBAAkB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wCAAwC;AACxC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,IAWzC,WAAW;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,mBA+J1C"}
@@ -0,0 +1,132 @@
1
+ import { handleCompaction } from "../services/compaction.js";
2
+ import { resolveContextLimit } from "../services/context-limit.js";
3
+ import { logger } from "../services/logger.js";
4
+ import { isTextPart, makeUserGroupId } from "../utils.js";
5
+ /** Creates the `event` hook handler. */
6
+ export function createEventHandler(deps) {
7
+ const { sessionManager, client, defaultGroupId, sdkClient, directory, groupIdPrefix, } = deps;
8
+ const defaultUserGroupId = makeUserGroupId(groupIdPrefix);
9
+ return async ({ event }) => {
10
+ try {
11
+ if (event.type === "session.created") {
12
+ const info = event.properties.info;
13
+ const sessionId = info.id;
14
+ const parentId = info.parentID ?? null;
15
+ const isMain = !parentId;
16
+ sessionManager.setParentId(sessionId, parentId);
17
+ logger.info("Session created:", {
18
+ sessionId,
19
+ isMain,
20
+ parentID: info.parentID,
21
+ });
22
+ if (isMain) {
23
+ sessionManager.setState(sessionId, {
24
+ groupId: defaultGroupId,
25
+ userGroupId: defaultUserGroupId,
26
+ injectedMemories: false,
27
+ lastInjectionMessageCount: 0,
28
+ messageCount: 0,
29
+ pendingMessages: [],
30
+ contextLimit: 200_000,
31
+ isMain,
32
+ });
33
+ }
34
+ else {
35
+ logger.debug("Ignoring subagent session:", sessionId);
36
+ }
37
+ return;
38
+ }
39
+ if (event.type === "session.compacted") {
40
+ const sessionId = event.properties.sessionID;
41
+ const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
42
+ if (!resolved) {
43
+ logger.debug("Unable to resolve session compaction:", sessionId);
44
+ return;
45
+ }
46
+ if (!state?.isMain) {
47
+ logger.debug("Ignoring non-main compaction:", sessionId);
48
+ return;
49
+ }
50
+ const summary = event.properties.summary ||
51
+ "";
52
+ await sessionManager.flushPendingMessages(sessionId, "Buffered messages flushed before compaction", 0);
53
+ if (summary) {
54
+ await handleCompaction({
55
+ client,
56
+ groupId: state.groupId,
57
+ summary,
58
+ sessionId,
59
+ });
60
+ }
61
+ return;
62
+ }
63
+ if (event.type === "session.idle") {
64
+ const sessionId = event.properties.sessionID;
65
+ const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
66
+ if (!resolved) {
67
+ logger.debug("Unable to resolve idle session:", sessionId);
68
+ return;
69
+ }
70
+ if (!state?.isMain) {
71
+ logger.debug("Ignoring non-main idle session:", sessionId);
72
+ return;
73
+ }
74
+ await sessionManager.flushPendingMessages(sessionId, "Buffered messages from OpenCode session", 50);
75
+ return;
76
+ }
77
+ if (event.type === "message.updated") {
78
+ const info = event.properties.info;
79
+ const sessionId = info.sessionID;
80
+ logger.info("Message event fired", {
81
+ hook: "message.updated",
82
+ eventType: "message.updated",
83
+ sessionId,
84
+ role: info.role,
85
+ messageID: info.id,
86
+ });
87
+ const { state, resolved } = await sessionManager.resolveSessionState(sessionId);
88
+ if (!resolved) {
89
+ logger.debug("Unable to resolve session for message update:", {
90
+ sessionId,
91
+ messageID: info.id,
92
+ role: info.role,
93
+ });
94
+ return;
95
+ }
96
+ if (!state?.isMain) {
97
+ logger.debug("Ignoring non-main message update:", sessionId);
98
+ return;
99
+ }
100
+ if (info.role !== "assistant") {
101
+ sessionManager.deletePendingAssistant(sessionId, info.id);
102
+ return;
103
+ }
104
+ const time = info.time;
105
+ if (!time?.completed)
106
+ return;
107
+ if (sessionManager.isAssistantBuffered(sessionId, info.id))
108
+ return;
109
+ sessionManager.finalizeAssistantMessage(state, sessionId, info.id, "message.updated");
110
+ if (info.tokens && info.providerID && info.modelID) {
111
+ resolveContextLimit(info.providerID, info.modelID, sdkClient, directory)
112
+ .then((limit) => {
113
+ state.contextLimit = limit;
114
+ })
115
+ .catch((err) => logger.debug("Failed to resolve context limit", err));
116
+ }
117
+ return;
118
+ }
119
+ if (event.type === "message.part.updated") {
120
+ const part = event.properties.part;
121
+ if (!isTextPart(part))
122
+ return;
123
+ const sessionId = part.sessionID;
124
+ const messageId = part.messageID;
125
+ sessionManager.bufferAssistantPart(sessionId, messageId, part.text);
126
+ }
127
+ }
128
+ catch (err) {
129
+ logger.error("Event handler error", { type: event.type, err });
130
+ }
131
+ };
132
+ }
@@ -1,4 +1,7 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const makeGroupId: (prefix: string, directory?: string) => string;
2
+ export { makeGroupId } from "./utils.js";
3
+ /**
4
+ * OpenCode plugin entry point for Graphiti memory integration.
5
+ */
3
6
  export declare const graphiti: Plugin;
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAqB/D,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAKhE,CAAC;AAeF,eAAO,MAAM,QAAQ,EAAE,MA+ftB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAW/D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,MA4CtB,CAAC"}