opencode-graphiti 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/esm/_dnt.shims.d.ts +6 -0
  4. package/esm/_dnt.shims.d.ts.map +1 -0
  5. package/esm/_dnt.shims.js +61 -0
  6. package/esm/mod.d.ts +2 -0
  7. package/esm/mod.d.ts.map +1 -0
  8. package/esm/mod.js +1 -0
  9. package/esm/package.json +3 -0
  10. package/esm/src/config.d.ts +3 -0
  11. package/esm/src/config.d.ts.map +1 -0
  12. package/esm/src/config.js +43 -0
  13. package/esm/src/index.d.ts +4 -0
  14. package/esm/src/index.d.ts.map +1 -0
  15. package/esm/src/index.js +436 -0
  16. package/esm/src/services/client.d.ts +37 -0
  17. package/esm/src/services/client.d.ts.map +1 -0
  18. package/esm/src/services/client.js +201 -0
  19. package/esm/src/services/compaction.d.ts +93 -0
  20. package/esm/src/services/compaction.d.ts.map +1 -0
  21. package/esm/src/services/compaction.js +144 -0
  22. package/esm/src/services/context.d.ts +3 -0
  23. package/esm/src/services/context.d.ts.map +1 -0
  24. package/esm/src/services/context.js +36 -0
  25. package/esm/src/services/logger.d.ts +7 -0
  26. package/esm/src/services/logger.d.ts.map +1 -0
  27. package/esm/src/services/logger.js +22 -0
  28. package/esm/src/types/index.d.ts +48 -0
  29. package/esm/src/types/index.d.ts.map +1 -0
  30. package/esm/src/types/index.js +1 -0
  31. package/package.json +34 -0
  32. package/script/_dnt.shims.d.ts +6 -0
  33. package/script/_dnt.shims.d.ts.map +1 -0
  34. package/script/_dnt.shims.js +65 -0
  35. package/script/mod.d.ts +2 -0
  36. package/script/mod.d.ts.map +1 -0
  37. package/script/mod.js +17 -0
  38. package/script/package.json +3 -0
  39. package/script/src/config.d.ts +3 -0
  40. package/script/src/config.d.ts.map +1 -0
  41. package/script/src/config.js +79 -0
  42. package/script/src/index.d.ts +4 -0
  43. package/script/src/index.d.ts.map +1 -0
  44. package/script/src/index.js +441 -0
  45. package/script/src/services/client.d.ts +37 -0
  46. package/script/src/services/client.d.ts.map +1 -0
  47. package/script/src/services/client.js +238 -0
  48. package/script/src/services/compaction.d.ts +93 -0
  49. package/script/src/services/compaction.d.ts.map +1 -0
  50. package/script/src/services/compaction.js +149 -0
  51. package/script/src/services/context.d.ts +3 -0
  52. package/script/src/services/context.d.ts.map +1 -0
  53. package/script/src/services/context.js +39 -0
  54. package/script/src/services/logger.d.ts +7 -0
  55. package/script/src/services/logger.d.ts.map +1 -0
  56. package/script/src/services/logger.js +61 -0
  57. package/script/src/types/index.d.ts +48 -0
  58. package/script/src/types/index.d.ts.map +1 -0
  59. package/script/src/types/index.js +2 -0
@@ -0,0 +1,441 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.graphiti = exports.makeGroupId = void 0;
4
+ const config_js_1 = require("./config.js");
5
+ const client_js_1 = require("./services/client.js");
6
+ const compaction_js_1 = require("./services/compaction.js");
7
+ const context_js_1 = require("./services/context.js");
8
+ const logger_js_1 = require("./services/logger.js");
9
+ const makeGroupId = (prefix, directory) => {
10
+ const parts = directory?.split("/").filter(Boolean) ?? [];
11
+ const projectName = parts[parts.length - 1] || "default";
12
+ const rawGroupId = `${prefix}_${projectName}`;
13
+ return rawGroupId.replace(/[^A-Za-z0-9_-]/g, "_");
14
+ };
15
+ exports.makeGroupId = makeGroupId;
16
+ const isTextPart = (value) => {
17
+ if (!value || typeof value !== "object")
18
+ return false;
19
+ const part = value;
20
+ return part.type === "text" && typeof part.text === "string" &&
21
+ !part.synthetic;
22
+ };
23
+ const extractTextFromParts = (parts) => parts.filter(isTextPart).map((part) => part.text).join(" ").trim();
24
+ const graphiti = async (input) => {
25
+ const config = (0, config_js_1.loadConfig)();
26
+ const client = new client_js_1.GraphitiClient(config.endpoint);
27
+ const sdkClient = input.client;
28
+ const connected = await client.connect();
29
+ if (!connected) {
30
+ logger_js_1.logger.warn("Could not connect to Graphiti MCP server at", config.endpoint);
31
+ logger_js_1.logger.warn("Memory features will be unavailable until connection is established");
32
+ }
33
+ const defaultGroupId = (0, exports.makeGroupId)(config.groupIdPrefix, input.directory);
34
+ logger_js_1.logger.info("Plugin initialized. Group ID:", defaultGroupId);
35
+ const sessions = new Map();
36
+ const parentIdCache = new Map();
37
+ const pendingAssistantMessages = new Map();
38
+ const bufferedAssistantMessageIds = new Set();
39
+ const resolveParentId = async (sessionId) => {
40
+ if (parentIdCache.has(sessionId)) {
41
+ return parentIdCache.get(sessionId) ?? null;
42
+ }
43
+ try {
44
+ const response = await sdkClient.session.get({
45
+ path: { id: sessionId },
46
+ });
47
+ const sessionInfo = typeof response === "object" && response !== null &&
48
+ "data" in response
49
+ ? response.data
50
+ : response;
51
+ if (!sessionInfo)
52
+ return undefined;
53
+ const parentId = sessionInfo.parentID ?? null;
54
+ parentIdCache.set(sessionId, parentId);
55
+ return parentId;
56
+ }
57
+ catch (err) {
58
+ logger_js_1.logger.debug("Failed to resolve session parentID", { sessionId, err });
59
+ return undefined;
60
+ }
61
+ };
62
+ const resolveSessionState = async (sessionId) => {
63
+ const parentId = await resolveParentId(sessionId);
64
+ if (parentId === undefined)
65
+ return { state: null, resolved: false };
66
+ if (parentId) {
67
+ sessions.delete(sessionId);
68
+ return { state: null, resolved: true };
69
+ }
70
+ let state = sessions.get(sessionId);
71
+ if (!state) {
72
+ state = {
73
+ groupId: defaultGroupId,
74
+ injectedMemories: false,
75
+ messageCount: 0,
76
+ pendingMessages: [],
77
+ isMain: true,
78
+ };
79
+ sessions.set(sessionId, state);
80
+ }
81
+ return { state, resolved: true };
82
+ };
83
+ const isSubagentSession = async (sessionId) => {
84
+ const parentId = await resolveParentId(sessionId);
85
+ return !!parentId;
86
+ };
87
+ const fetchLatestAssistantMessage = async (sessionId) => {
88
+ try {
89
+ const response = await sdkClient.session.messages({
90
+ sessionID: sessionId,
91
+ limit: 20,
92
+ });
93
+ const payload = response && typeof response === "object" &&
94
+ "data" in response
95
+ ? response.data
96
+ : response;
97
+ const messages = Array.isArray(payload)
98
+ ? payload
99
+ : [];
100
+ if (messages.length === 0)
101
+ return null;
102
+ const lastAssistant = [...messages]
103
+ .reverse()
104
+ .find((message) => message.info?.role === "assistant");
105
+ if (!lastAssistant)
106
+ return null;
107
+ const text = extractTextFromParts(lastAssistant.parts);
108
+ if (!text)
109
+ return null;
110
+ return { id: lastAssistant.info?.id, text };
111
+ }
112
+ catch (err) {
113
+ logger_js_1.logger.debug("Failed to list session messages for fallback", {
114
+ sessionId,
115
+ err,
116
+ });
117
+ return null;
118
+ }
119
+ };
120
+ const finalizeAssistantMessage = (state, sessionId, messageId, source) => {
121
+ const key = `${sessionId}:${messageId}`;
122
+ if (bufferedAssistantMessageIds.has(key))
123
+ return;
124
+ const buffered = pendingAssistantMessages.get(key);
125
+ pendingAssistantMessages.delete(key);
126
+ bufferedAssistantMessageIds.add(key);
127
+ const messageText = buffered?.text?.trim() ?? "";
128
+ const messagePreview = messageText.slice(0, 120);
129
+ logger_js_1.logger.info("Assistant message completed", {
130
+ hook: source,
131
+ sessionId,
132
+ messageID: messageId,
133
+ source,
134
+ messageLength: messageText.length,
135
+ preview: messagePreview,
136
+ });
137
+ if (!messageText) {
138
+ logger_js_1.logger.debug("Assistant message completed without buffered text", {
139
+ hook: source,
140
+ sessionId,
141
+ messageID: messageId,
142
+ source,
143
+ });
144
+ return;
145
+ }
146
+ state.pendingMessages.push(`Assistant: ${messageText}`);
147
+ logger_js_1.logger.info("Buffered assistant reply", {
148
+ hook: source,
149
+ sessionId,
150
+ messageID: messageId,
151
+ source,
152
+ messageLength: messageText.length,
153
+ preview: messagePreview,
154
+ });
155
+ };
156
+ const flushPendingMessages = async (sessionId, sourceDescription, minBytes) => {
157
+ const state = sessions.get(sessionId);
158
+ if (!state || state.pendingMessages.length === 0)
159
+ return;
160
+ const lastMessage = state.pendingMessages.at(-1);
161
+ if (lastMessage) {
162
+ const separatorIndex = lastMessage.indexOf(":");
163
+ const role = separatorIndex === -1
164
+ ? lastMessage.trim().toLowerCase()
165
+ : lastMessage.slice(0, separatorIndex).trim().toLowerCase();
166
+ if (role === "user") {
167
+ const fallback = await fetchLatestAssistantMessage(sessionId);
168
+ if (fallback?.text) {
169
+ const fallbackKey = fallback.id
170
+ ? `${sessionId}:${fallback.id}`
171
+ : undefined;
172
+ const alreadyBuffered = fallbackKey
173
+ ? bufferedAssistantMessageIds.has(fallbackKey)
174
+ : state.pendingMessages.some((message) => message.startsWith("Assistant:") &&
175
+ message.includes(fallback.text));
176
+ if (!alreadyBuffered) {
177
+ state.pendingMessages.push(`Assistant: ${fallback.text}`);
178
+ if (fallbackKey) {
179
+ bufferedAssistantMessageIds.add(fallbackKey);
180
+ }
181
+ logger_js_1.logger.info("Fallback assistant fetch used", {
182
+ sessionId,
183
+ messageID: fallback.id,
184
+ messageLength: fallback.text.length,
185
+ });
186
+ }
187
+ }
188
+ }
189
+ }
190
+ const combined = state.pendingMessages.join("\n\n");
191
+ if (combined.length < minBytes)
192
+ return;
193
+ const messagesToFlush = [...state.pendingMessages];
194
+ state.pendingMessages = [];
195
+ const messageLines = messagesToFlush.map((message) => {
196
+ const separatorIndex = message.indexOf(":");
197
+ const role = separatorIndex === -1
198
+ ? "Unknown"
199
+ : message.slice(0, separatorIndex).trim();
200
+ const text = separatorIndex === -1
201
+ ? message
202
+ : message.slice(separatorIndex + 1).trim();
203
+ return `${role}: ${text}`;
204
+ });
205
+ try {
206
+ const name = combined.slice(0, 80).replace(/\n/g, " ");
207
+ logger_js_1.logger.info(`Flushing ${messagesToFlush.length} buffered message(s).`);
208
+ logger_js_1.logger.info(`Buffered message contents:\n${messageLines.join("\n")}`, { sessionId });
209
+ await client.addEpisode({
210
+ name: `Buffered messages: ${name}`,
211
+ episodeBody: combined,
212
+ groupId: state.groupId,
213
+ source: "text",
214
+ sourceDescription,
215
+ });
216
+ logger_js_1.logger.info("Flushed buffered messages to Graphiti");
217
+ }
218
+ catch (err) {
219
+ logger_js_1.logger.error(`Failed to flush messages for ${sessionId}:`, err);
220
+ const currentState = sessions.get(sessionId);
221
+ if (currentState) {
222
+ currentState.pendingMessages = [
223
+ ...messagesToFlush,
224
+ ...currentState.pendingMessages,
225
+ ];
226
+ }
227
+ }
228
+ };
229
+ const preemptiveCompaction = (0, compaction_js_1.createPreemptiveCompactionHandler)({
230
+ compactionThreshold: config.compactionThreshold,
231
+ minTokensForCompaction: config.minTokensForCompaction,
232
+ compactionCooldownMs: config.compactionCooldownMs,
233
+ autoResumeAfterCompaction: config.autoResumeAfterCompaction,
234
+ }, {
235
+ sdkClient: sdkClient,
236
+ directory: input.directory,
237
+ });
238
+ return {
239
+ event: async ({ event }) => {
240
+ try {
241
+ if (event.type === "session.created") {
242
+ const info = event.properties.info;
243
+ const sessionId = info.id;
244
+ const parentId = info.parentID ?? null;
245
+ const isMain = !parentId;
246
+ parentIdCache.set(sessionId, parentId);
247
+ logger_js_1.logger.info("Session created:", {
248
+ sessionId,
249
+ isMain,
250
+ parentID: info.parentID,
251
+ });
252
+ if (isMain) {
253
+ sessions.set(sessionId, {
254
+ groupId: defaultGroupId,
255
+ injectedMemories: false,
256
+ messageCount: 0,
257
+ pendingMessages: [],
258
+ isMain,
259
+ });
260
+ }
261
+ else {
262
+ logger_js_1.logger.debug("Ignoring subagent session:", sessionId);
263
+ }
264
+ return;
265
+ }
266
+ if (event.type === "session.compacted") {
267
+ const sessionId = event.properties.sessionID;
268
+ const { state, resolved } = await resolveSessionState(sessionId);
269
+ if (!resolved) {
270
+ logger_js_1.logger.debug("Unable to resolve session compaction:", sessionId);
271
+ return;
272
+ }
273
+ if (!state?.isMain) {
274
+ logger_js_1.logger.debug("Ignoring non-main compaction:", sessionId);
275
+ return;
276
+ }
277
+ const summary = event.properties.summary ||
278
+ "";
279
+ await flushPendingMessages(sessionId, "Buffered messages flushed before compaction", 0);
280
+ if (summary) {
281
+ await (0, compaction_js_1.handleCompaction)({
282
+ client,
283
+ config,
284
+ groupId: state.groupId,
285
+ summary,
286
+ sessionId,
287
+ });
288
+ }
289
+ return;
290
+ }
291
+ if (event.type === "session.idle") {
292
+ const sessionId = event.properties.sessionID;
293
+ const { state, resolved } = await resolveSessionState(sessionId);
294
+ if (!resolved) {
295
+ logger_js_1.logger.debug("Unable to resolve idle session:", sessionId);
296
+ return;
297
+ }
298
+ if (!state?.isMain) {
299
+ logger_js_1.logger.debug("Ignoring non-main idle session:", sessionId);
300
+ return;
301
+ }
302
+ await flushPendingMessages(sessionId, "Buffered messages from OpenCode session", 50);
303
+ return;
304
+ }
305
+ if (event.type === "message.updated") {
306
+ const info = event.properties.info;
307
+ const sessionId = info.sessionID;
308
+ logger_js_1.logger.info("Message event fired", {
309
+ hook: "message.updated",
310
+ eventType: "message.updated",
311
+ sessionId,
312
+ role: info.role,
313
+ messageID: info.id,
314
+ });
315
+ const { state, resolved } = await resolveSessionState(sessionId);
316
+ if (!resolved) {
317
+ logger_js_1.logger.debug("Unable to resolve session for message update:", {
318
+ sessionId,
319
+ messageID: info.id,
320
+ role: info.role,
321
+ });
322
+ return;
323
+ }
324
+ if (!state?.isMain) {
325
+ logger_js_1.logger.debug("Ignoring non-main message update:", sessionId);
326
+ return;
327
+ }
328
+ if (info.role !== "assistant") {
329
+ pendingAssistantMessages.delete(`${sessionId}:${info.id}`);
330
+ return;
331
+ }
332
+ const key = `${sessionId}:${info.id}`;
333
+ const time = info.time;
334
+ if (!time?.completed)
335
+ return;
336
+ if (bufferedAssistantMessageIds.has(key))
337
+ return;
338
+ finalizeAssistantMessage(state, sessionId, info.id, "message.updated");
339
+ if (info.tokens && info.providerID && info.modelID) {
340
+ preemptiveCompaction
341
+ .checkAndTriggerCompaction(sessionId, info.tokens, info.providerID, info.modelID)
342
+ .catch((err) => logger_js_1.logger.error("Preemptive compaction check failed", err));
343
+ }
344
+ return;
345
+ }
346
+ if (event.type === "message.part.updated") {
347
+ const part = event.properties.part;
348
+ if (part.type !== "text" || part.synthetic)
349
+ return;
350
+ const sessionId = part.sessionID;
351
+ const messageId = part.messageID;
352
+ const key = `${sessionId}:${messageId}`;
353
+ pendingAssistantMessages.set(key, {
354
+ sessionId,
355
+ text: part.text,
356
+ });
357
+ }
358
+ }
359
+ catch (err) {
360
+ logger_js_1.logger.error("Event handler error", { type: event.type, err });
361
+ }
362
+ },
363
+ "chat.message": async ({ sessionID }, output) => {
364
+ if (await isSubagentSession(sessionID)) {
365
+ logger_js_1.logger.debug("Ignoring subagent chat message:", sessionID);
366
+ return;
367
+ }
368
+ const { state, resolved } = await resolveSessionState(sessionID);
369
+ if (!resolved) {
370
+ output.allow_buffering = true;
371
+ logger_js_1.logger.debug("Unable to resolve session for message:", { sessionID });
372
+ return;
373
+ }
374
+ if (!state?.isMain) {
375
+ logger_js_1.logger.debug("Ignoring subagent chat message:", sessionID);
376
+ return;
377
+ }
378
+ state.messageCount++;
379
+ const messageText = extractTextFromParts(output.parts);
380
+ if (!messageText)
381
+ return;
382
+ state.pendingMessages.push(`User: ${messageText}`);
383
+ logger_js_1.logger.info("Buffered user message", {
384
+ hook: "chat.message",
385
+ sessionID,
386
+ messageLength: messageText.length,
387
+ });
388
+ if (!state.injectedMemories && config.injectOnFirstMessage) {
389
+ state.injectedMemories = true;
390
+ try {
391
+ const [facts, nodes] = await Promise.all([
392
+ client.searchFacts({
393
+ query: messageText,
394
+ groupIds: [state.groupId],
395
+ maxFacts: config.maxFacts,
396
+ }),
397
+ client.searchNodes({
398
+ query: messageText,
399
+ groupIds: [state.groupId],
400
+ maxNodes: config.maxNodes,
401
+ }),
402
+ ]);
403
+ const memoryContext = (0, context_js_1.formatMemoryContext)(facts, nodes);
404
+ if (memoryContext) {
405
+ output.parts.unshift({
406
+ type: "text",
407
+ text: memoryContext,
408
+ id: `graphiti-memory-${Date.now()}`,
409
+ sessionID: output.message.sessionID,
410
+ messageID: output.message.id,
411
+ synthetic: true,
412
+ });
413
+ logger_js_1.logger.info(`Injected ${facts.length} facts and ${nodes.length} nodes`);
414
+ }
415
+ }
416
+ catch (err) {
417
+ logger_js_1.logger.error("Failed to inject memories:", err);
418
+ }
419
+ }
420
+ },
421
+ "experimental.session.compacting": async ({ sessionID }, output) => {
422
+ const state = sessions.get(sessionID);
423
+ if (!state?.isMain) {
424
+ logger_js_1.logger.debug("Ignoring non-main compaction context:", sessionID);
425
+ return;
426
+ }
427
+ const groupId = state.groupId || defaultGroupId;
428
+ const additionalContext = await (0, compaction_js_1.getCompactionContext)({
429
+ client,
430
+ config,
431
+ groupId,
432
+ contextStrings: output.context,
433
+ });
434
+ if (additionalContext.length > 0) {
435
+ output.context.push(...additionalContext);
436
+ logger_js_1.logger.info("Injected persistent knowledge into compaction context");
437
+ }
438
+ },
439
+ };
440
+ };
441
+ exports.graphiti = graphiti;
@@ -0,0 +1,37 @@
1
+ import type { GraphitiEpisode, GraphitiFact, GraphitiNode } from "../types/index.js";
2
+ export declare class GraphitiClient {
3
+ private client;
4
+ private transport;
5
+ private connected;
6
+ private endpoint;
7
+ constructor(endpoint: string);
8
+ connect(): Promise<boolean>;
9
+ disconnect(): Promise<void>;
10
+ private callTool;
11
+ private isSessionExpired;
12
+ private reconnect;
13
+ parseToolResult(result: unknown): unknown;
14
+ addEpisode(params: {
15
+ name: string;
16
+ episodeBody: string;
17
+ groupId?: string;
18
+ source?: "text" | "json" | "message";
19
+ sourceDescription?: string;
20
+ }): Promise<void>;
21
+ searchFacts(params: {
22
+ query: string;
23
+ groupIds?: string[];
24
+ maxFacts?: number;
25
+ }): Promise<GraphitiFact[]>;
26
+ searchNodes(params: {
27
+ query: string;
28
+ groupIds?: string[];
29
+ maxNodes?: number;
30
+ }): Promise<GraphitiNode[]>;
31
+ getEpisodes(params: {
32
+ groupIds?: string[];
33
+ maxEpisodes?: number;
34
+ }): Promise<GraphitiEpisode[]>;
35
+ getStatus(): Promise<boolean>;
36
+ }
37
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/services/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EAEZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAG3B,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,MAAM;IAStB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAa3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAOnB,QAAQ;IAkCtB,OAAO,CAAC,gBAAgB;YASV,SAAS;IAgBvB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;IAyBnC,UAAU,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,OAAO,CAAC,IAAI,CAAC;IAWX,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsBrB,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsBrB,WAAW,CAAC,MAAM,EAAE;QACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAaxB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"}