@codemem/opencode-plugin 0.38.0-alpha.2 → 0.39.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.
- package/.opencode/plugins/codemem.js +389 -9
- package/package.json +2 -2
|
@@ -15,9 +15,13 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const TRUTHY_VALUES = ["1", "true", "yes"];
|
|
17
17
|
const DISABLED_VALUES = ["0", "false", "off"];
|
|
18
|
-
const PINNED_BACKEND_VERSION = "0.
|
|
18
|
+
const PINNED_BACKEND_VERSION = "0.39.0";
|
|
19
19
|
const COMPAT_CHECK_DELAY_MS = 1500;
|
|
20
20
|
const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
21
|
+
const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
|
|
22
|
+
const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
|
|
23
|
+
const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
|
|
24
|
+
const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
|
|
21
25
|
|
|
22
26
|
let compatCheckCache = null;
|
|
23
27
|
|
|
@@ -27,6 +31,16 @@ const envHasValue = (value, truthyValues) =>
|
|
|
27
31
|
const envNotDisabled = (value) =>
|
|
28
32
|
!DISABLED_VALUES.includes(normalizeEnvValue(value));
|
|
29
33
|
|
|
34
|
+
const resolveInjectSurface = (value) => {
|
|
35
|
+
const normalized = String(value || "message").trim().toLowerCase();
|
|
36
|
+
if (normalized === "system") {
|
|
37
|
+
return "system";
|
|
38
|
+
}
|
|
39
|
+
return "message";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
|
|
43
|
+
|
|
30
44
|
const DEFAULT_LOG_PATH = (homeDir, cwd) => `${homeDir || cwd}/.codemem/plugin.log`;
|
|
31
45
|
|
|
32
46
|
const resolveLogPath = (logPathEnvRaw, cwd, homeDir) => {
|
|
@@ -284,6 +298,256 @@ const applyInjectedContextToOutput = async ({
|
|
|
284
298
|
return true;
|
|
285
299
|
};
|
|
286
300
|
|
|
301
|
+
const isUserMessageEntry = (entry) => entry?.info?.role === "user";
|
|
302
|
+
|
|
303
|
+
const resolveEntryMessageId = (entry) => {
|
|
304
|
+
const id = entry?.info?.id || entry?.parts?.find((part) => part?.messageID)?.messageID;
|
|
305
|
+
return id ? String(id) : null;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const fallbackEntryMessageId = (entry, index = 0) => {
|
|
309
|
+
const id = resolveEntryMessageId(entry);
|
|
310
|
+
if (id) return id;
|
|
311
|
+
const text = extractMessageText(entry);
|
|
312
|
+
if (text) {
|
|
313
|
+
return `message-${createHash("sha256").update(text).digest("hex").slice(0, 16)}`;
|
|
314
|
+
}
|
|
315
|
+
return `message-${index}`;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const resolveEntrySessionID = (entry) => {
|
|
319
|
+
const id = entry?.info?.sessionID || entry?.parts?.find((part) => part?.sessionID)?.sessionID;
|
|
320
|
+
return id ? String(id) : null;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const isCodememContextPart = (part) =>
|
|
324
|
+
part?.type === "text" && String(part?.id || "").startsWith(CODEMEM_CONTEXT_PART_ID_PREFIX);
|
|
325
|
+
|
|
326
|
+
const extractMessageText = (entry) => {
|
|
327
|
+
if (!Array.isArray(entry?.parts)) {
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
return entry.parts
|
|
331
|
+
.filter((part) => part?.type === "text" && !isCodememContextPart(part))
|
|
332
|
+
.map((part) => String(part?.text || "").trim())
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
.join("\n")
|
|
335
|
+
.trim();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const findFirstUserMessage = (messages) => {
|
|
339
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
340
|
+
const entry = messages[index];
|
|
341
|
+
if (isUserMessageEntry(entry)) {
|
|
342
|
+
return { entry, index };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const findLatestUserMessage = (messages) => {
|
|
349
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
350
|
+
const entry = messages[index];
|
|
351
|
+
if (isUserMessageEntry(entry)) {
|
|
352
|
+
return { entry, index };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const markCompactionInjectionSkip = (compactionInjectionSkips, sessionID, now = Date.now()) => {
|
|
359
|
+
if (!compactionInjectionSkips || !sessionID) return;
|
|
360
|
+
compactionInjectionSkips.set(String(sessionID), now + COMPACTION_INJECTION_SKIP_TTL_MS);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const consumeCompactionInjectionSkip = (compactionInjectionSkips, sessionID, now = Date.now()) => {
|
|
364
|
+
if (!compactionInjectionSkips || !sessionID) return false;
|
|
365
|
+
const key = String(sessionID);
|
|
366
|
+
const expiresAt = compactionInjectionSkips.get(key);
|
|
367
|
+
if (!expiresAt) return false;
|
|
368
|
+
compactionInjectionSkips.delete(key);
|
|
369
|
+
return now <= expiresAt;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// OpenCode does not guarantee that synthetic message parts survive into the
|
|
373
|
+
// next transform call. Cache each message's injected block by message ID and
|
|
374
|
+
// re-append the exact same bytes on later turns: older prompts stay stable for
|
|
375
|
+
// provider prefix caching while only the newest user message triggers a fresh
|
|
376
|
+
// pack build. The replay cache only activates when OpenCode provides both a
|
|
377
|
+
// session ID and a real message ID; unidentified messages still receive current
|
|
378
|
+
// turn context, but are not cached because positional fallbacks can drift across
|
|
379
|
+
// turns or sessions. Do not "simplify" this into recomputing previous turns.
|
|
380
|
+
const getSessionMessageInjectionCache = (messageInjectionCache, sessionID) => {
|
|
381
|
+
if (!sessionID) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const cacheKey = sessionID;
|
|
385
|
+
let sessionCache = messageInjectionCache.get(cacheKey);
|
|
386
|
+
if (sessionCache) {
|
|
387
|
+
// Refresh recency so the bounded cache behaves like a small LRU.
|
|
388
|
+
messageInjectionCache.delete(cacheKey);
|
|
389
|
+
} else {
|
|
390
|
+
sessionCache = new Map();
|
|
391
|
+
}
|
|
392
|
+
messageInjectionCache.set(cacheKey, sessionCache);
|
|
393
|
+
while (messageInjectionCache.size > MAX_MESSAGE_INJECTION_CACHE_SESSIONS) {
|
|
394
|
+
const oldestKey = messageInjectionCache.keys().next().value;
|
|
395
|
+
if (!oldestKey) break;
|
|
396
|
+
messageInjectionCache.delete(oldestKey);
|
|
397
|
+
}
|
|
398
|
+
return sessionCache;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const trimSessionMessageInjectionCache = (sessionCache) => {
|
|
402
|
+
if (!sessionCache) return;
|
|
403
|
+
while (sessionCache.size > MAX_MESSAGE_INJECTION_CACHE_MESSAGES) {
|
|
404
|
+
const oldestKey = sessionCache.keys().next().value;
|
|
405
|
+
if (!oldestKey) break;
|
|
406
|
+
sessionCache.delete(oldestKey);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const normalizeInjectedMessageParts = (messages, sessionCache) => {
|
|
411
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
412
|
+
const entry = messages[index];
|
|
413
|
+
if (!isUserMessageEntry(entry) || !Array.isArray(entry.parts)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const messageId = resolveEntryMessageId(entry);
|
|
418
|
+
const retainedParts = [];
|
|
419
|
+
let existingText = null;
|
|
420
|
+
for (const part of entry.parts) {
|
|
421
|
+
if (isCodememContextPart(part)) {
|
|
422
|
+
const text = String(part?.text || "");
|
|
423
|
+
if (text.trim()) {
|
|
424
|
+
existingText = text;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
retainedParts.push(part);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (existingText && sessionCache && messageId && !sessionCache.has(messageId)) {
|
|
431
|
+
sessionCache.set(messageId, existingText);
|
|
432
|
+
trimSessionMessageInjectionCache(sessionCache);
|
|
433
|
+
}
|
|
434
|
+
if (retainedParts.length !== entry.parts.length) {
|
|
435
|
+
entry.parts.splice(0, entry.parts.length, ...retainedParts);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const buildInjectedContextPart = (entry, index, text, options = {}) => {
|
|
441
|
+
const messageId = options.messageId || fallbackEntryMessageId(entry, index);
|
|
442
|
+
const sessionID = options.sessionID || resolveEntrySessionID(entry) || "unknown";
|
|
443
|
+
return {
|
|
444
|
+
id: `${CODEMEM_CONTEXT_PART_ID_PREFIX}${messageId}`,
|
|
445
|
+
sessionID,
|
|
446
|
+
messageID: messageId,
|
|
447
|
+
type: "text",
|
|
448
|
+
text,
|
|
449
|
+
synthetic: true,
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const appendCachedInjectedContextParts = (messages, sessionCache, sessionID = null) => {
|
|
454
|
+
let appended = 0;
|
|
455
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
456
|
+
const entry = messages[index];
|
|
457
|
+
if (!isUserMessageEntry(entry) || !Array.isArray(entry.parts)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const messageId = resolveEntryMessageId(entry);
|
|
461
|
+
if (!messageId) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const text = sessionCache.get(messageId);
|
|
465
|
+
if (!text) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
entry.parts.push(buildInjectedContextPart(entry, index, text, { messageId, sessionID }));
|
|
469
|
+
appended += 1;
|
|
470
|
+
}
|
|
471
|
+
return appended;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const applyInjectedContextToMessages = async ({
|
|
475
|
+
injectEnabled,
|
|
476
|
+
input,
|
|
477
|
+
output,
|
|
478
|
+
injectionToastShown,
|
|
479
|
+
showToast,
|
|
480
|
+
resolveInjectQuery,
|
|
481
|
+
buildInjectedContext,
|
|
482
|
+
messageInjectionCache,
|
|
483
|
+
compactionInjectionSkips,
|
|
484
|
+
}) => {
|
|
485
|
+
if (!injectEnabled || !Array.isArray(output?.messages)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const latestUser = findLatestUserMessage(output.messages);
|
|
490
|
+
const sessionID = latestUser
|
|
491
|
+
? resolveEntrySessionID(latestUser.entry) || input?.sessionID || null
|
|
492
|
+
: input?.sessionID || null;
|
|
493
|
+
|
|
494
|
+
if (consumeCompactionInjectionSkip(compactionInjectionSkips, sessionID)) {
|
|
495
|
+
normalizeInjectedMessageParts(output.messages, null);
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!latestUser) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const sessionCache = getSessionMessageInjectionCache(messageInjectionCache, sessionID);
|
|
504
|
+
normalizeInjectedMessageParts(output.messages, sessionCache);
|
|
505
|
+
|
|
506
|
+
const latestMessageId = resolveEntryMessageId(latestUser.entry);
|
|
507
|
+
const canReplay = Boolean(sessionCache && latestMessageId);
|
|
508
|
+
if (!canReplay || !sessionCache.has(latestMessageId)) {
|
|
509
|
+
const firstUser = findFirstUserMessage(output.messages);
|
|
510
|
+
const query = resolveInjectQuery({
|
|
511
|
+
firstPrompt: firstUser ? extractMessageText(firstUser.entry) : null,
|
|
512
|
+
lastPromptText: extractMessageText(latestUser.entry),
|
|
513
|
+
});
|
|
514
|
+
const injected = await buildInjectedContext(query);
|
|
515
|
+
if (injected?.text) {
|
|
516
|
+
if (canReplay) {
|
|
517
|
+
sessionCache.set(latestMessageId, injected.text);
|
|
518
|
+
trimSessionMessageInjectionCache(sessionCache);
|
|
519
|
+
} else if (Array.isArray(latestUser.entry.parts)) {
|
|
520
|
+
latestUser.entry.parts.push(
|
|
521
|
+
buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
|
|
522
|
+
messageId: latestMessageId || undefined,
|
|
523
|
+
sessionID: sessionID || undefined,
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const toastKey = sessionID || latestMessageId || "unknown";
|
|
529
|
+
if (!injectionToastShown.has(toastKey) && showToast) {
|
|
530
|
+
injectionToastShown.add(toastKey);
|
|
531
|
+
try {
|
|
532
|
+
await showToast(buildInjectionToastMessage(injected.metrics));
|
|
533
|
+
} catch {
|
|
534
|
+
// best-effort only
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!canReplay) {
|
|
541
|
+
return Boolean(
|
|
542
|
+
Array.isArray(latestUser.entry.parts)
|
|
543
|
+
&& latestUser.entry.parts.some(isCodememContextPart)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const appended = appendCachedInjectedContextParts(output.messages, sessionCache, sessionID);
|
|
548
|
+
return appended > 0;
|
|
549
|
+
};
|
|
550
|
+
|
|
287
551
|
const mapOpencodeEventTypeToAdapterType = (eventType) => {
|
|
288
552
|
if (eventType === "user_prompt") {
|
|
289
553
|
return "prompt";
|
|
@@ -589,7 +853,7 @@ const parsePositiveInt = (value, fallback) => {
|
|
|
589
853
|
};
|
|
590
854
|
|
|
591
855
|
export const buildInjectionToastMessage = (metrics) => {
|
|
592
|
-
const items = asFiniteNonNegativeInt(metrics?.items);
|
|
856
|
+
const items = asFiniteNonNegativeInt(metrics?.items) ?? asFiniteNonNegativeInt(metrics?.total_items);
|
|
593
857
|
const packTokens = asFiniteNonNegativeInt(metrics?.pack_tokens);
|
|
594
858
|
const avoided = asFiniteNonNegativeInt(metrics?.avoided_work_tokens);
|
|
595
859
|
const avoidedUnknown = asNonNegativeCount(metrics?.avoided_work_unknown_items);
|
|
@@ -756,12 +1020,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
756
1020
|
const injectEnabled = envNotDisabled(
|
|
757
1021
|
process.env.CODEMEM_INJECT_CONTEXT || "1"
|
|
758
1022
|
);
|
|
1023
|
+
const injectSurface = resolveInjectSurface(process.env.CODEMEM_INJECT_SURFACE);
|
|
759
1024
|
// Only use env overrides if explicitly set; otherwise CLI uses config defaults
|
|
760
1025
|
const injectLimitEnv = process.env.CODEMEM_INJECT_LIMIT;
|
|
761
1026
|
const injectLimit = injectLimitEnv ? parseNumber(injectLimitEnv, null) : null;
|
|
762
1027
|
const injectTokenBudgetEnv = process.env.CODEMEM_INJECT_TOKEN_BUDGET;
|
|
763
1028
|
const injectTokenBudget = injectTokenBudgetEnv ? parseNumber(injectTokenBudgetEnv, null) : null;
|
|
764
1029
|
const injectionToastShown = new Set();
|
|
1030
|
+
const messageInjectionCache = new Map();
|
|
1031
|
+
const compactionInjectionSkips = new Map();
|
|
765
1032
|
let sessionStartedAt = null;
|
|
766
1033
|
let activeSessionID = null;
|
|
767
1034
|
let viewerStarted = false;
|
|
@@ -1052,6 +1319,20 @@ export const OpencodeMemPlugin = async ({
|
|
|
1052
1319
|
return event?.properties?.sessionID || null;
|
|
1053
1320
|
};
|
|
1054
1321
|
|
|
1322
|
+
const extractHookSessionID = (input) => {
|
|
1323
|
+
if (!input || typeof input !== "object") {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
return (
|
|
1327
|
+
input.sessionID ||
|
|
1328
|
+
input.sessionId ||
|
|
1329
|
+
input.session?.id ||
|
|
1330
|
+
input.session?.sessionID ||
|
|
1331
|
+
input.properties?.sessionID ||
|
|
1332
|
+
null
|
|
1333
|
+
);
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1055
1336
|
// Session context tracking for comprehensive memories
|
|
1056
1337
|
const sessionContext = {
|
|
1057
1338
|
firstPrompt: null,
|
|
@@ -1500,21 +1781,34 @@ export const OpencodeMemPlugin = async ({
|
|
|
1500
1781
|
await showToast(`${message}. Suggested action: ${guidance.action}`, "warning");
|
|
1501
1782
|
};
|
|
1502
1783
|
|
|
1503
|
-
const resolveInjectQuery = () =>
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1784
|
+
const resolveInjectQuery = (overrides = {}) => {
|
|
1785
|
+
const firstPrompt = hasOwn(overrides, "firstPrompt")
|
|
1786
|
+
? overrides.firstPrompt
|
|
1787
|
+
: sessionContext.firstPrompt;
|
|
1788
|
+
const resolvedLastPromptText = hasOwn(overrides, "lastPromptText")
|
|
1789
|
+
? overrides.lastPromptText
|
|
1790
|
+
: lastPromptText;
|
|
1791
|
+
return buildInjectQuery({
|
|
1792
|
+
firstPrompt,
|
|
1793
|
+
lastPromptText: resolvedLastPromptText,
|
|
1507
1794
|
projectName: resolveProjectName(project, cwd),
|
|
1508
1795
|
filesModified: sessionContext.filesModified,
|
|
1509
1796
|
});
|
|
1797
|
+
};
|
|
1510
1798
|
|
|
1511
|
-
const describeInjectQuery = (query) => {
|
|
1799
|
+
const describeInjectQuery = (query, overrides = {}) => {
|
|
1512
1800
|
const safeQuery = redactLog((query || "").trim(), 240);
|
|
1513
1801
|
const projectName = resolveProjectName(project, cwd) || "";
|
|
1802
|
+
const firstPrompt = hasOwn(overrides, "firstPrompt")
|
|
1803
|
+
? overrides.firstPrompt
|
|
1804
|
+
: sessionContext.firstPrompt;
|
|
1805
|
+
const resolvedLastPromptText = hasOwn(overrides, "lastPromptText")
|
|
1806
|
+
? overrides.lastPromptText
|
|
1807
|
+
: lastPromptText;
|
|
1514
1808
|
return {
|
|
1515
1809
|
safeQuery,
|
|
1516
|
-
firstPromptLen:
|
|
1517
|
-
lastPromptLen:
|
|
1810
|
+
firstPromptLen: firstPrompt?.trim()?.length || 0,
|
|
1811
|
+
lastPromptLen: resolvedLastPromptText?.trim()?.length || 0,
|
|
1518
1812
|
projectName,
|
|
1519
1813
|
filesModifiedCount: sessionContext.filesModified.size,
|
|
1520
1814
|
};
|
|
@@ -1806,7 +2100,87 @@ export const OpencodeMemPlugin = async ({
|
|
|
1806
2100
|
};
|
|
1807
2101
|
|
|
1808
2102
|
return {
|
|
2103
|
+
"experimental.session.compacting": async (input) => {
|
|
2104
|
+
const sessionID = extractHookSessionID(input) || activeSessionID;
|
|
2105
|
+
markCompactionInjectionSkip(compactionInjectionSkips, sessionID);
|
|
2106
|
+
if (debug) {
|
|
2107
|
+
await logLine(
|
|
2108
|
+
`inject.compaction_skip_marked sessionID=${sessionID || "unknown"}`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
},
|
|
2112
|
+
"experimental.chat.messages.transform": async (input, output) => {
|
|
2113
|
+
if (injectSurface === "system") {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const latestUser = Array.isArray(output?.messages)
|
|
2117
|
+
? findLatestUserMessage(output.messages)
|
|
2118
|
+
: null;
|
|
2119
|
+
const sessionID = latestUser ? resolveEntrySessionID(latestUser.entry) : input?.sessionID;
|
|
2120
|
+
const latestPromptText = latestUser ? extractMessageText(latestUser.entry) : "";
|
|
2121
|
+
if (debug) {
|
|
2122
|
+
const query = resolveInjectQuery({ lastPromptText: latestPromptText });
|
|
2123
|
+
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
2124
|
+
describeInjectQuery(query, { lastPromptText: latestPromptText });
|
|
2125
|
+
await logLine(
|
|
2126
|
+
`inject.messages_transform sessionID=${sessionID || "unknown"} query_len=${
|
|
2127
|
+
query ? query.length : 0
|
|
2128
|
+
} inject_enabled=${injectEnabled} tui_toast=${Boolean(client.tui?.showToast)} query=${JSON.stringify(safeQuery)} first_prompt_len=${firstPromptLen} last_prompt_len=${lastPromptLen} project=${JSON.stringify(projectName)} files_modified=${filesModifiedCount}`
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
let applied = false;
|
|
2133
|
+
try {
|
|
2134
|
+
applied = await applyInjectedContextToMessages({
|
|
2135
|
+
injectEnabled,
|
|
2136
|
+
input,
|
|
2137
|
+
output,
|
|
2138
|
+
injectionToastShown,
|
|
2139
|
+
showToast: client.tui?.showToast
|
|
2140
|
+
? async (message) => {
|
|
2141
|
+
await client.tui.showToast({
|
|
2142
|
+
body: {
|
|
2143
|
+
message,
|
|
2144
|
+
variant: "info",
|
|
2145
|
+
},
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
: null,
|
|
2149
|
+
resolveInjectQuery,
|
|
2150
|
+
buildInjectedContext,
|
|
2151
|
+
messageInjectionCache,
|
|
2152
|
+
compactionInjectionSkips,
|
|
2153
|
+
});
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
await logLine(
|
|
2156
|
+
`inject.messages_transform.error sessionID=${sessionID || "unknown"} message=${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
if (debug) {
|
|
2160
|
+
const partsCount = Array.isArray(output?.messages)
|
|
2161
|
+
? output.messages.reduce(
|
|
2162
|
+
(count, entry) => count + (Array.isArray(entry?.parts) ? entry.parts.length : 0),
|
|
2163
|
+
0
|
|
2164
|
+
)
|
|
2165
|
+
: 0;
|
|
2166
|
+
await logLine(
|
|
2167
|
+
`inject.messages_transform.result sessionID=${sessionID || "unknown"} applied=${Boolean(applied)} messages=${Array.isArray(output?.messages) ? output.messages.length : 0} parts=${partsCount}`
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
},
|
|
1809
2171
|
"experimental.chat.system.transform": async (input, output) => {
|
|
2172
|
+
if (injectSurface !== "system") {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const hookSessionID = input?.sessionID || activeSessionID;
|
|
2176
|
+
if (consumeCompactionInjectionSkip(compactionInjectionSkips, hookSessionID)) {
|
|
2177
|
+
if (debug) {
|
|
2178
|
+
await logLine(
|
|
2179
|
+
`inject.transform.skip_compaction sessionID=${hookSessionID || "unknown"}`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
1810
2184
|
const query = resolveInjectQuery();
|
|
1811
2185
|
if (debug) {
|
|
1812
2186
|
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
@@ -2033,6 +2407,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2033
2407
|
activeSessionID = null;
|
|
2034
2408
|
if (sessionID) {
|
|
2035
2409
|
injectionToastShown.delete(sessionID);
|
|
2410
|
+
messageInjectionCache.delete(sessionID);
|
|
2411
|
+
compactionInjectionSkips.delete(sessionID);
|
|
2036
2412
|
}
|
|
2037
2413
|
await stopViewer();
|
|
2038
2414
|
}
|
|
@@ -2143,7 +2519,11 @@ export const __testUtils = {
|
|
|
2143
2519
|
buildPackArgs,
|
|
2144
2520
|
parsePackText,
|
|
2145
2521
|
parsePackMetrics,
|
|
2522
|
+
resolveInjectSurface,
|
|
2146
2523
|
applyInjectedContextToOutput,
|
|
2524
|
+
applyInjectedContextToMessages,
|
|
2525
|
+
extractMessageText,
|
|
2526
|
+
isCodememContextPart,
|
|
2147
2527
|
buildRunnerArgs,
|
|
2148
2528
|
appendWorkingSetFileArgs,
|
|
2149
2529
|
extractApplyPatchPaths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/opencode-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "CodeMem plugin for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
".opencode/lib/"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@opencode-ai/plugin": "1.17.
|
|
21
|
+
"@opencode-ai/plugin": "1.17.13"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|