Skip to content

Commit 22a8a77

Browse files
authored
Merge pull request #239 from EyJunge1/fix/issues-231-232-236-237
fix: profile, capture, and compaction regressions
2 parents bbf23a5 + 3582631 commit 22a8a77

19 files changed

Lines changed: 1599 additions & 269 deletions

‎src/config.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface OpenCodeMemConfig {
4242
autoCaptureMaxIterations?: number;
4343
autoCaptureIterationTimeout?: number;
4444
autoCaptureMaxRetries?: number;
45+
autoCaptureMaxContextBytes?: number;
4546
autoCaptureLanguage?: string;
4647
memoryProvider?: "openai-chat" | "openai-responses" | "anthropic" | "minimax";
4748
memoryModel?: string;
@@ -81,6 +82,8 @@ interface OpenCodeMemConfig {
8182
userProfileCentroidDriftThreshold?: number;
8283
userProfileEmbeddingMinDescriptionLength?: number;
8384
userProfileMinEvidenceForRetention?: number;
85+
userProfileAutoCleanupEnabled?: boolean;
86+
userProfileAutoCleanupInterval?: number;
8487
userProfileValidationEnabled?: boolean;
8588
showAutoCaptureToasts?: boolean;
8689
showUserProfileToasts?: boolean;
@@ -152,6 +155,7 @@ const DEFAULTS: Required<
152155
autoCaptureMaxIterations: 5,
153156
autoCaptureIterationTimeout: 30000,
154157
autoCaptureMaxRetries: 3,
158+
autoCaptureMaxContextBytes: 131072,
155159
aiSessionRetentionDays: 7,
156160
webServerEnabled: true,
157161
webServerPort: 4747,
@@ -179,6 +183,8 @@ const DEFAULTS: Required<
179183
userProfileCentroidDriftThreshold: 0.65,
180184
userProfileEmbeddingMinDescriptionLength: 5,
181185
userProfileMinEvidenceForRetention: 3,
186+
userProfileAutoCleanupEnabled: true,
187+
userProfileAutoCleanupInterval: 100,
182188
userProfileValidationEnabled: false,
183189
showAutoCaptureToasts: true,
184190
showUserProfileToasts: true,
@@ -410,6 +416,11 @@ const CONFIG_TEMPLATE = `{
410416
411417
// Maximum number of times to retry capturing a prompt if it fails (due to network, API errors, etc.)
412418
"autoCaptureMaxRetries": 3,
419+
420+
// Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.
421+
// Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).
422+
// Rough guide: tokens ≈ bytes / 4 for mixed code/prose.
423+
"autoCaptureMaxContextBytes": 131072,
413424
414425
// Days to keep AI session history before cleanup
415426
"aiSessionRetentionDays": 7,
@@ -488,6 +499,11 @@ const CONFIG_TEMPLATE = `{
488499
// Items confirmed fewer times are more likely to be pruned when confidence decays
489500
"userProfileMinEvidenceForRetention": 3,
490501
502+
// Periodically merge duplicate or irrelevant profile items with the configured AI provider
503+
"userProfileAutoCleanupEnabled": true,
504+
// Number of analyzed user prompts between automatic AI cleanup runs
505+
"userProfileAutoCleanupInterval": 100,
506+
491507
// Enable LLM validation of existing preferences against recent behavior.
492508
// When enabled, each analysis round checks if top-5 preferences still match recent prompts.
493509
// Experimental — disabled by default.
@@ -568,11 +584,23 @@ function getEmbeddingDimensions(model: string): number {
568584
return dimensionMap[model] || 768;
569585
}
570586

587+
export function normalizeAutoCaptureMaxContextBytes(value: number): number {
588+
if (!Number.isInteger(value) || value < 16384 || value > 16 * 1024 * 1024) {
589+
throw new Error(`Invalid autoCaptureMaxContextBytes config: ${value}`);
590+
}
591+
return value;
592+
}
593+
571594
function buildConfig(fileConfig: OpenCodeMemConfig) {
572595
const memoryApiKey = resolveSecretValue(fileConfig.memoryApiKey);
573596
const embeddingDimensions =
574597
fileConfig.embeddingDimensions ??
575598
getEmbeddingDimensions(fileConfig.embeddingModel ?? DEFAULTS.embeddingModel);
599+
const autoCaptureMaxContextBytes = normalizeAutoCaptureMaxContextBytes(
600+
fileConfig.autoCaptureMaxContextBytes ?? DEFAULTS.autoCaptureMaxContextBytes
601+
);
602+
const userProfileAutoCleanupInterval =
603+
fileConfig.userProfileAutoCleanupInterval ?? DEFAULTS.userProfileAutoCleanupInterval;
576604

577605
if (
578606
!Number.isInteger(embeddingDimensions) ||
@@ -581,6 +609,11 @@ function buildConfig(fileConfig: OpenCodeMemConfig) {
581609
) {
582610
throw new Error(`Invalid embeddingDimensions config: ${embeddingDimensions}`);
583611
}
612+
if (!Number.isInteger(userProfileAutoCleanupInterval) || userProfileAutoCleanupInterval <= 0) {
613+
throw new Error(
614+
`Invalid userProfileAutoCleanupInterval config: ${userProfileAutoCleanupInterval}`
615+
);
616+
}
584617

585618
return {
586619
storagePath: expandPath(fileConfig.storagePath ?? DEFAULTS.storagePath),
@@ -605,6 +638,7 @@ function buildConfig(fileConfig: OpenCodeMemConfig) {
605638
autoCaptureIterationTimeout:
606639
fileConfig.autoCaptureIterationTimeout ?? DEFAULTS.autoCaptureIterationTimeout,
607640
autoCaptureMaxRetries: fileConfig.autoCaptureMaxRetries ?? DEFAULTS.autoCaptureMaxRetries,
641+
autoCaptureMaxContextBytes,
608642
autoCaptureLanguage: fileConfig.autoCaptureLanguage,
609643
memoryProvider: (fileConfig.memoryProvider ?? "openai-chat") as
610644
"openai-chat" | "openai-responses" | "anthropic" | "minimax",
@@ -677,6 +711,9 @@ function buildConfig(fileConfig: OpenCodeMemConfig) {
677711
DEFAULTS.userProfileEmbeddingMinDescriptionLength,
678712
userProfileMinEvidenceForRetention:
679713
fileConfig.userProfileMinEvidenceForRetention ?? DEFAULTS.userProfileMinEvidenceForRetention,
714+
userProfileAutoCleanupEnabled:
715+
fileConfig.userProfileAutoCleanupEnabled ?? DEFAULTS.userProfileAutoCleanupEnabled,
716+
userProfileAutoCleanupInterval,
680717
userProfileValidationEnabled:
681718
fileConfig.userProfileValidationEnabled ?? DEFAULTS.userProfileValidationEnabled,
682719
userProfileStaleDays: fileConfig.userProfileStaleDays ?? DEFAULTS.userProfileStaleDays,

‎src/index.ts‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,92 @@ function extractSessionTitle(response: unknown): string | undefined {
5454
return obj.data?.title ?? obj.title;
5555
}
5656

57+
function unwrapSdkData<T>(response: unknown): T | undefined {
58+
if (!response || typeof response !== "object") return undefined;
59+
const obj = response as { data?: T };
60+
return (obj.data ?? response) as T;
61+
}
62+
63+
/**
64+
* Resolve the session's active agent so compaction memory injection does not
65+
* reset OpenCode to the stock "general-purpose" fallback (issue #236).
66+
*
67+
* Preference order:
68+
* 1. session.get().agent (v2 hosts)
69+
* 2. Latest non-compaction user message agent
70+
* 3. Latest non-compaction / non-summary assistant mode (v1) or agent (v2)
71+
*/
72+
export async function resolveSessionAgent(
73+
client: unknown,
74+
sessionID: string
75+
): Promise<string | undefined> {
76+
const sessionClient = (
77+
client as {
78+
session?: {
79+
get?: (args: unknown) => Promise<unknown>;
80+
messages?: (args: unknown) => Promise<unknown>;
81+
};
82+
}
83+
)?.session;
84+
85+
if (typeof sessionClient?.get === "function") {
86+
try {
87+
const session = unwrapSdkData<{ agent?: string }>(
88+
await sessionClient.get({ path: { id: sessionID } })
89+
);
90+
if (typeof session?.agent === "string" && session.agent.trim()) {
91+
return session.agent.trim();
92+
}
93+
} catch (error) {
94+
log("resolveSessionAgent: session.get failed", { sessionID, error: String(error) });
95+
}
96+
}
97+
98+
if (typeof sessionClient?.messages !== "function") {
99+
return undefined;
100+
}
101+
102+
try {
103+
const messages = unwrapSdkData<
104+
Array<{
105+
info?: {
106+
role?: string;
107+
agent?: string;
108+
mode?: string;
109+
summary?: boolean;
110+
};
111+
}>
112+
>(await sessionClient.messages({ path: { id: sessionID } }));
113+
114+
if (!Array.isArray(messages)) return undefined;
115+
116+
for (let i = messages.length - 1; i >= 0; i--) {
117+
const info = messages[i]?.info;
118+
if (!info) continue;
119+
120+
if (info.role === "user") {
121+
if (typeof info.agent === "string" && info.agent.trim()) {
122+
return info.agent.trim();
123+
}
124+
continue;
125+
}
126+
127+
if (info.role === "assistant") {
128+
if (info.summary === true || info.mode === "compaction") continue;
129+
const agent =
130+
(typeof info.agent === "string" && info.agent.trim()) ||
131+
(typeof info.mode === "string" && info.mode.trim()) ||
132+
undefined;
133+
if (agent) return agent;
134+
}
135+
}
136+
} catch (error) {
137+
log("resolveSessionAgent: session.messages failed", { sessionID, error: String(error) });
138+
}
139+
140+
return undefined;
141+
}
142+
57143
async function isInternalCaptureSession(client: unknown, sessionID: string): Promise<boolean> {
58144
// Fast path: sessions we created ourselves (survives brief post-delete window).
59145
if (isTrackedInternalCaptureSession(sessionID)) {
@@ -873,12 +959,23 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => {
873959
}
874960

875961
const memoryContext = formatMemoriesForCompaction(memoriesResult.results);
962+
const agent = await resolveSessionAgent(ctx.client, sessionID);
963+
if (!agent) {
964+
log(
965+
"Compaction: skipped memory injection because session agent could not be resolved",
966+
{
967+
sessionID,
968+
}
969+
);
970+
return;
971+
}
876972

877973
await ctx.client.session.prompt({
878974
path: { id: sessionID },
879975
body: {
880976
parts: [{ id: `prt-compaction-${Date.now()}`, type: "text", text: memoryContext }],
881977
noReply: true,
978+
agent,
882979
},
883980
});
884981

@@ -898,6 +995,7 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => {
898995
log("Compaction memory injected", {
899996
sessionID,
900997
count: memoriesResult.results.length,
998+
agent: agent ?? null,
901999
});
9021000
} catch (error) {
9031001
log("Compaction handler error", { error: String(error) });

‎src/services/ai/validators/user-profile-validator.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ export class UserProfileValidator {
6262
if (!pref.description || typeof pref.description !== "string") {
6363
errors.push(`preferences[${i}].description is missing or invalid`);
6464
}
65-
if (typeof pref.confidence !== "number") {
66-
errors.push(`preferences[${i}].confidence is missing or invalid`);
65+
if (
66+
typeof pref.confidence !== "number" ||
67+
!Number.isFinite(pref.confidence) ||
68+
pref.confidence < 0 ||
69+
pref.confidence > 1
70+
) {
71+
errors.push(`preferences[${i}].confidence must be a finite number between 0 and 1`);
6772
}
6873
if (!Array.isArray(pref.evidence)) {
6974
errors.push(`preferences[${i}].evidence must be an array`);

0 commit comments

Comments
 (0)