@codemem/opencode-plugin 0.38.0 → 0.40.0-alpha.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.
- package/.opencode/plugins/codemem.js +400 -10
- 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.40.0-alpha.1";
|
|
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);
|
|
@@ -622,7 +886,14 @@ const detectRunner = ({ cwd, envRunner }) => {
|
|
|
622
886
|
}
|
|
623
887
|
// Prefer the TS codemem if installed globally, fall back to npx
|
|
624
888
|
try {
|
|
625
|
-
const versionOutput = execSync("codemem --version", {
|
|
889
|
+
const versionOutput = execSync("codemem --version", {
|
|
890
|
+
encoding: "utf-8",
|
|
891
|
+
timeout: 3000,
|
|
892
|
+
// Suppress shell "not found" noise when codemem is not on PATH; the
|
|
893
|
+
// catch below falls back to npx. Without this, stderr leaks to the
|
|
894
|
+
// terminal on every OpenCode startup for npx-only installs.
|
|
895
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
896
|
+
}).trim();
|
|
626
897
|
// Accept any global codemem at or above the pinned backend version, so a
|
|
627
898
|
// newer global install is used instead of silently downgrading to
|
|
628
899
|
// `npx codemem@<pin>` (the old check only matched the exact pin or the
|
|
@@ -756,12 +1027,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
756
1027
|
const injectEnabled = envNotDisabled(
|
|
757
1028
|
process.env.CODEMEM_INJECT_CONTEXT || "1"
|
|
758
1029
|
);
|
|
1030
|
+
const injectSurface = resolveInjectSurface(process.env.CODEMEM_INJECT_SURFACE);
|
|
759
1031
|
// Only use env overrides if explicitly set; otherwise CLI uses config defaults
|
|
760
1032
|
const injectLimitEnv = process.env.CODEMEM_INJECT_LIMIT;
|
|
761
1033
|
const injectLimit = injectLimitEnv ? parseNumber(injectLimitEnv, null) : null;
|
|
762
1034
|
const injectTokenBudgetEnv = process.env.CODEMEM_INJECT_TOKEN_BUDGET;
|
|
763
1035
|
const injectTokenBudget = injectTokenBudgetEnv ? parseNumber(injectTokenBudgetEnv, null) : null;
|
|
764
1036
|
const injectionToastShown = new Set();
|
|
1037
|
+
const messageInjectionCache = new Map();
|
|
1038
|
+
const compactionInjectionSkips = new Map();
|
|
765
1039
|
let sessionStartedAt = null;
|
|
766
1040
|
let activeSessionID = null;
|
|
767
1041
|
let viewerStarted = false;
|
|
@@ -1052,6 +1326,20 @@ export const OpencodeMemPlugin = async ({
|
|
|
1052
1326
|
return event?.properties?.sessionID || null;
|
|
1053
1327
|
};
|
|
1054
1328
|
|
|
1329
|
+
const extractHookSessionID = (input) => {
|
|
1330
|
+
if (!input || typeof input !== "object") {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
return (
|
|
1334
|
+
input.sessionID ||
|
|
1335
|
+
input.sessionId ||
|
|
1336
|
+
input.session?.id ||
|
|
1337
|
+
input.session?.sessionID ||
|
|
1338
|
+
input.properties?.sessionID ||
|
|
1339
|
+
null
|
|
1340
|
+
);
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1055
1343
|
// Session context tracking for comprehensive memories
|
|
1056
1344
|
const sessionContext = {
|
|
1057
1345
|
firstPrompt: null,
|
|
@@ -1500,21 +1788,34 @@ export const OpencodeMemPlugin = async ({
|
|
|
1500
1788
|
await showToast(`${message}. Suggested action: ${guidance.action}`, "warning");
|
|
1501
1789
|
};
|
|
1502
1790
|
|
|
1503
|
-
const resolveInjectQuery = () =>
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1791
|
+
const resolveInjectQuery = (overrides = {}) => {
|
|
1792
|
+
const firstPrompt = hasOwn(overrides, "firstPrompt")
|
|
1793
|
+
? overrides.firstPrompt
|
|
1794
|
+
: sessionContext.firstPrompt;
|
|
1795
|
+
const resolvedLastPromptText = hasOwn(overrides, "lastPromptText")
|
|
1796
|
+
? overrides.lastPromptText
|
|
1797
|
+
: lastPromptText;
|
|
1798
|
+
return buildInjectQuery({
|
|
1799
|
+
firstPrompt,
|
|
1800
|
+
lastPromptText: resolvedLastPromptText,
|
|
1507
1801
|
projectName: resolveProjectName(project, cwd),
|
|
1508
1802
|
filesModified: sessionContext.filesModified,
|
|
1509
1803
|
});
|
|
1804
|
+
};
|
|
1510
1805
|
|
|
1511
|
-
const describeInjectQuery = (query) => {
|
|
1806
|
+
const describeInjectQuery = (query, overrides = {}) => {
|
|
1512
1807
|
const safeQuery = redactLog((query || "").trim(), 240);
|
|
1513
1808
|
const projectName = resolveProjectName(project, cwd) || "";
|
|
1809
|
+
const firstPrompt = hasOwn(overrides, "firstPrompt")
|
|
1810
|
+
? overrides.firstPrompt
|
|
1811
|
+
: sessionContext.firstPrompt;
|
|
1812
|
+
const resolvedLastPromptText = hasOwn(overrides, "lastPromptText")
|
|
1813
|
+
? overrides.lastPromptText
|
|
1814
|
+
: lastPromptText;
|
|
1514
1815
|
return {
|
|
1515
1816
|
safeQuery,
|
|
1516
|
-
firstPromptLen:
|
|
1517
|
-
lastPromptLen:
|
|
1817
|
+
firstPromptLen: firstPrompt?.trim()?.length || 0,
|
|
1818
|
+
lastPromptLen: resolvedLastPromptText?.trim()?.length || 0,
|
|
1518
1819
|
projectName,
|
|
1519
1820
|
filesModifiedCount: sessionContext.filesModified.size,
|
|
1520
1821
|
};
|
|
@@ -1652,6 +1953,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
1652
1953
|
cwd: runnerFrom,
|
|
1653
1954
|
timeout: 500,
|
|
1654
1955
|
encoding: "utf-8",
|
|
1956
|
+
// Suppress "fatal: not a git repository" when the working directory is
|
|
1957
|
+
// not a git repo. The catch below leaves version as "unknown".
|
|
1958
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1655
1959
|
}).trim();
|
|
1656
1960
|
} catch (err) {
|
|
1657
1961
|
// Ignore - version will remain 'unknown'
|
|
@@ -1806,7 +2110,87 @@ export const OpencodeMemPlugin = async ({
|
|
|
1806
2110
|
};
|
|
1807
2111
|
|
|
1808
2112
|
return {
|
|
2113
|
+
"experimental.session.compacting": async (input) => {
|
|
2114
|
+
const sessionID = extractHookSessionID(input) || activeSessionID;
|
|
2115
|
+
markCompactionInjectionSkip(compactionInjectionSkips, sessionID);
|
|
2116
|
+
if (debug) {
|
|
2117
|
+
await logLine(
|
|
2118
|
+
`inject.compaction_skip_marked sessionID=${sessionID || "unknown"}`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
"experimental.chat.messages.transform": async (input, output) => {
|
|
2123
|
+
if (injectSurface === "system") {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const latestUser = Array.isArray(output?.messages)
|
|
2127
|
+
? findLatestUserMessage(output.messages)
|
|
2128
|
+
: null;
|
|
2129
|
+
const sessionID = latestUser ? resolveEntrySessionID(latestUser.entry) : input?.sessionID;
|
|
2130
|
+
const latestPromptText = latestUser ? extractMessageText(latestUser.entry) : "";
|
|
2131
|
+
if (debug) {
|
|
2132
|
+
const query = resolveInjectQuery({ lastPromptText: latestPromptText });
|
|
2133
|
+
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
2134
|
+
describeInjectQuery(query, { lastPromptText: latestPromptText });
|
|
2135
|
+
await logLine(
|
|
2136
|
+
`inject.messages_transform sessionID=${sessionID || "unknown"} query_len=${
|
|
2137
|
+
query ? query.length : 0
|
|
2138
|
+
} 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}`
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
let applied = false;
|
|
2143
|
+
try {
|
|
2144
|
+
applied = await applyInjectedContextToMessages({
|
|
2145
|
+
injectEnabled,
|
|
2146
|
+
input,
|
|
2147
|
+
output,
|
|
2148
|
+
injectionToastShown,
|
|
2149
|
+
showToast: client.tui?.showToast
|
|
2150
|
+
? async (message) => {
|
|
2151
|
+
await client.tui.showToast({
|
|
2152
|
+
body: {
|
|
2153
|
+
message,
|
|
2154
|
+
variant: "info",
|
|
2155
|
+
},
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
: null,
|
|
2159
|
+
resolveInjectQuery,
|
|
2160
|
+
buildInjectedContext,
|
|
2161
|
+
messageInjectionCache,
|
|
2162
|
+
compactionInjectionSkips,
|
|
2163
|
+
});
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
await logLine(
|
|
2166
|
+
`inject.messages_transform.error sessionID=${sessionID || "unknown"} message=${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
if (debug) {
|
|
2170
|
+
const partsCount = Array.isArray(output?.messages)
|
|
2171
|
+
? output.messages.reduce(
|
|
2172
|
+
(count, entry) => count + (Array.isArray(entry?.parts) ? entry.parts.length : 0),
|
|
2173
|
+
0
|
|
2174
|
+
)
|
|
2175
|
+
: 0;
|
|
2176
|
+
await logLine(
|
|
2177
|
+
`inject.messages_transform.result sessionID=${sessionID || "unknown"} applied=${Boolean(applied)} messages=${Array.isArray(output?.messages) ? output.messages.length : 0} parts=${partsCount}`
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
1809
2181
|
"experimental.chat.system.transform": async (input, output) => {
|
|
2182
|
+
if (injectSurface !== "system") {
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const hookSessionID = input?.sessionID || activeSessionID;
|
|
2186
|
+
if (consumeCompactionInjectionSkip(compactionInjectionSkips, hookSessionID)) {
|
|
2187
|
+
if (debug) {
|
|
2188
|
+
await logLine(
|
|
2189
|
+
`inject.transform.skip_compaction sessionID=${hookSessionID || "unknown"}`
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
1810
2194
|
const query = resolveInjectQuery();
|
|
1811
2195
|
if (debug) {
|
|
1812
2196
|
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
@@ -2033,6 +2417,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2033
2417
|
activeSessionID = null;
|
|
2034
2418
|
if (sessionID) {
|
|
2035
2419
|
injectionToastShown.delete(sessionID);
|
|
2420
|
+
messageInjectionCache.delete(sessionID);
|
|
2421
|
+
compactionInjectionSkips.delete(sessionID);
|
|
2036
2422
|
}
|
|
2037
2423
|
await stopViewer();
|
|
2038
2424
|
}
|
|
@@ -2143,7 +2529,11 @@ export const __testUtils = {
|
|
|
2143
2529
|
buildPackArgs,
|
|
2144
2530
|
parsePackText,
|
|
2145
2531
|
parsePackMetrics,
|
|
2532
|
+
resolveInjectSurface,
|
|
2146
2533
|
applyInjectedContextToOutput,
|
|
2534
|
+
applyInjectedContextToMessages,
|
|
2535
|
+
extractMessageText,
|
|
2536
|
+
isCodememContextPart,
|
|
2147
2537
|
buildRunnerArgs,
|
|
2148
2538
|
appendWorkingSetFileArgs,
|
|
2149
2539
|
extractApplyPatchPaths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/opencode-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0-alpha.1",
|
|
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.
|
|
21
|
+
"@opencode-ai/plugin": "1.18.4"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|