Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# local design and implementation plans
docs/plans/
.sisyphus

openspec
.codegraph
.opencode
AGENTS.md
113 changes: 93 additions & 20 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,23 @@ interface OpenCodeMemConfig {
deduplicationEnabled?: boolean;
deduplicationSimilarityThreshold?: number;
userProfileAnalysisInterval?: number;
userProfileMaxPreferences?: number;
userProfileMaxPatterns?: number;
userProfileMaxWorkflows?: number;
userProfileDisplayPreferences?: number;
userProfileDisplayPatterns?: number;
userProfileDisplayWorkflows?: number;
userProfileStaleDays?: number;
userProfileInjectPreferences?: number;
userProfileInjectPatterns?: number;
userProfileInjectWorkflows?: number;
userProfileConfidenceDecayDays?: number;
userProfileChangelogRetentionCount?: number;
userProfileEmbeddingThresholdSameCat?: number;
userProfileEmbeddingThresholdSameCatWeak?: number;
userProfileEmbeddingThresholdCrossCat?: number;
userProfileEmbeddingThresholdCrossCatWeak?: number;
userProfileCentroidDriftThreshold?: number;
userProfileEmbeddingMinDescriptionLength?: number;
userProfileMinEvidenceForRetention?: number;
userProfileValidationEnabled?: boolean;
showAutoCaptureToasts?: boolean;
showUserProfileToasts?: boolean;
showErrorToasts?: boolean;
Expand Down Expand Up @@ -139,11 +151,23 @@ const DEFAULTS: Required<
deduplicationEnabled: true,
deduplicationSimilarityThreshold: 0.9,
userProfileAnalysisInterval: 10,
userProfileMaxPreferences: 20,
userProfileMaxPatterns: 15,
userProfileMaxWorkflows: 10,
userProfileDisplayPreferences: 20,
userProfileDisplayPatterns: 15,
userProfileDisplayWorkflows: 10,
userProfileStaleDays: 2,
userProfileInjectPreferences: 5,
userProfileInjectPatterns: 5,
userProfileInjectWorkflows: 3,
userProfileConfidenceDecayDays: 30,
userProfileChangelogRetentionCount: 5,
userProfileEmbeddingThresholdSameCat: 0.8,
userProfileEmbeddingThresholdSameCatWeak: 0.5,
userProfileEmbeddingThresholdCrossCat: 0.9,
userProfileEmbeddingThresholdCrossCatWeak: 0.8,
userProfileCentroidDriftThreshold: 0.65,
userProfileEmbeddingMinDescriptionLength: 5,
userProfileMinEvidenceForRetention: 3,
userProfileValidationEnabled: false,
showAutoCaptureToasts: true,
showUserProfileToasts: true,
showErrorToasts: true,
Expand Down Expand Up @@ -394,18 +418,28 @@ const CONFIG_TEMPLATE = `{
// - User workflows (development habits, sequences, learning style)
// - Skill level (overall and per-domain assessment)
"userProfileAnalysisInterval": 10,

// Days before inactive items (all types) are eligible for removal
"userProfileStaleDays": 2,

// Number of preferences shown in UI
"userProfileDisplayPreferences": 20,

// Number of patterns shown in UI
"userProfileDisplayPatterns": 15,

// Number of workflows shown in UI
"userProfileDisplayWorkflows": 10,

// Maximum number of preferences to keep in user profile (sorted by confidence)
// Preferences are things like "prefers code without comments", "likes concise responses"
"userProfileMaxPreferences": 20,
// Number of preferences injected into LLM conversation context
// Keep this small — the strongest signals are enough; more dilute LLM attention
"userProfileInjectPreferences": 5,

// Maximum number of patterns to keep in user profile (sorted by frequency)
// Patterns are recurring topics like "often asks about database optimization"
"userProfileMaxPatterns": 15,
// Number of patterns injected into LLM conversation context
"userProfileInjectPatterns": 5,

// Maximum number of workflows to keep in user profile (sorted by frequency)
// Workflows are sequences like "usually asks for tests after implementation"
"userProfileMaxWorkflows": 10,
// Number of workflows injected into LLM conversation context
"userProfileInjectWorkflows": 3,

// Days before preference confidence starts to decay (if not reinforced)
// Preferences that aren't seen again will gradually lose confidence and be removed
Expand All @@ -414,7 +448,16 @@ const CONFIG_TEMPLATE = `{
// Number of profile versions to keep in changelog (for rollback/debugging)
// Older versions are automatically cleaned up
"userProfileChangelogRetentionCount": 5,


// Minimum evidence count for a preference/pattern to survive confidence decay
// Items confirmed fewer times are more likely to be pruned when confidence decays
"userProfileMinEvidenceForRetention": 3,

// Enable LLM validation of existing preferences against recent behavior.
// When enabled, each analysis round checks if top-5 preferences still match recent prompts.
// Experimental — disabled by default.
"userProfileValidationEnabled": false,

// ============================================
// Search Settings
// ============================================
Expand Down Expand Up @@ -543,14 +586,44 @@ function buildConfig(fileConfig: OpenCodeMemConfig) {
fileConfig.deduplicationSimilarityThreshold ?? DEFAULTS.deduplicationSimilarityThreshold,
userProfileAnalysisInterval:
fileConfig.userProfileAnalysisInterval ?? DEFAULTS.userProfileAnalysisInterval,
userProfileMaxPreferences:
fileConfig.userProfileMaxPreferences ?? DEFAULTS.userProfileMaxPreferences,
userProfileMaxPatterns: fileConfig.userProfileMaxPatterns ?? DEFAULTS.userProfileMaxPatterns,
userProfileMaxWorkflows: fileConfig.userProfileMaxWorkflows ?? DEFAULTS.userProfileMaxWorkflows,
userProfileDisplayPreferences:
fileConfig.userProfileDisplayPreferences ?? DEFAULTS.userProfileDisplayPreferences,
userProfileDisplayPatterns:
fileConfig.userProfileDisplayPatterns ?? DEFAULTS.userProfileDisplayPatterns,
userProfileDisplayWorkflows:
fileConfig.userProfileDisplayWorkflows ?? DEFAULTS.userProfileDisplayWorkflows,
userProfileInjectPreferences:
fileConfig.userProfileInjectPreferences ?? DEFAULTS.userProfileInjectPreferences,
userProfileInjectPatterns:
fileConfig.userProfileInjectPatterns ?? DEFAULTS.userProfileInjectPatterns,
userProfileInjectWorkflows:
fileConfig.userProfileInjectWorkflows ?? DEFAULTS.userProfileInjectWorkflows,
userProfileConfidenceDecayDays:
fileConfig.userProfileConfidenceDecayDays ?? DEFAULTS.userProfileConfidenceDecayDays,
userProfileChangelogRetentionCount:
fileConfig.userProfileChangelogRetentionCount ?? DEFAULTS.userProfileChangelogRetentionCount,
userProfileEmbeddingThresholdSameCat:
fileConfig.userProfileEmbeddingThresholdSameCat ??
DEFAULTS.userProfileEmbeddingThresholdSameCat,
userProfileEmbeddingThresholdSameCatWeak:
fileConfig.userProfileEmbeddingThresholdSameCatWeak ??
DEFAULTS.userProfileEmbeddingThresholdSameCatWeak,
userProfileEmbeddingThresholdCrossCat:
fileConfig.userProfileEmbeddingThresholdCrossCat ??
DEFAULTS.userProfileEmbeddingThresholdCrossCat,
userProfileEmbeddingThresholdCrossCatWeak:
fileConfig.userProfileEmbeddingThresholdCrossCatWeak ??
DEFAULTS.userProfileEmbeddingThresholdCrossCatWeak,
userProfileCentroidDriftThreshold:
fileConfig.userProfileCentroidDriftThreshold ?? DEFAULTS.userProfileCentroidDriftThreshold,
userProfileEmbeddingMinDescriptionLength:
fileConfig.userProfileEmbeddingMinDescriptionLength ??
DEFAULTS.userProfileEmbeddingMinDescriptionLength,
userProfileMinEvidenceForRetention:
fileConfig.userProfileMinEvidenceForRetention ?? DEFAULTS.userProfileMinEvidenceForRetention,
userProfileValidationEnabled:
fileConfig.userProfileValidationEnabled ?? DEFAULTS.userProfileValidationEnabled,
userProfileStaleDays: fileConfig.userProfileStaleDays ?? DEFAULTS.userProfileStaleDays,
showAutoCaptureToasts: fileConfig.showAutoCaptureToasts ?? DEFAULTS.showAutoCaptureToasts,
showUserProfileToasts: fileConfig.showUserProfileToasts ?? DEFAULTS.showUserProfileToasts,
showErrorToasts: fileConfig.showErrorToasts ?? DEFAULTS.showErrorToasts,
Expand Down
22 changes: 18 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => {
const providerResult = await ctx.client.provider.list();
if (providerResult.data?.connected) {
setConnectedProviders(providerResult.data.connected);
log("opencode providers connected", {
list: providerResult.data.connected,
configured: CONFIG.opencodeProvider || "(not set)",
});
} else {
log("opencode provider list empty or failed", {
data: JSON.stringify(providerResult.data).substring(0, 100),
});
}
} catch (error) {
log("Failed to initialize opencode provider state", { error: String(error) });
Expand Down Expand Up @@ -398,17 +406,23 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => {
category: "explicit",
description: sanitizedContent,
confidence: 1.0,
frequency: 1,
evidence: ["manual-write"],
lastUpdated: Date.now(),
lastSeen: Date.now(),
};

const existingProfile = userProfileManager.getActiveProfile(userId);

if (existingProfile) {
const existingData = JSON.parse(existingProfile.profileData);
const mergedData = userProfileManager.mergeProfileData(existingData, {
preferences: [newPreference],
});
const mergedData = await userProfileManager.mergeProfileData(
existingData,
{
preferences: [newPreference],
},
undefined,
existingProfile.id
);
userProfileManager.updateProfile(
existingProfile.id,
mergedData,
Expand Down
37 changes: 37 additions & 0 deletions src/services/ai/profile-llm-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
import { CONFIG } from "../../config.js";

let _cachedClient: OpencodeClient | null = null;
let _cachedProvider: string | null = null;
let _cachedModel: string | null = null;

export async function getOpenCodeClient(): Promise<OpencodeClient> {
const provider = CONFIG.opencodeProvider!;
const model = CONFIG.opencodeModel!;

if (!provider || !model) {
throw new Error("opencode-mem: opencodeProvider and opencodeModel must be configured");
}

if (_cachedClient && _cachedProvider === provider && _cachedModel === model) {
return _cachedClient;
}

const { isProviderConnected, getV2Client } = await import("./opencode-provider.js");

if (!isProviderConnected(provider)) {
throw new Error(
`opencode provider '${provider}' is not connected. Check your opencode provider configuration.`
);
}

const client = getV2Client();
if (!client) {
throw new Error("opencode-mem: v2 client not initialized");
}

_cachedClient = client;
_cachedProvider = provider;
_cachedModel = model;
return client;
}
22 changes: 21 additions & 1 deletion src/services/ai/providers/openai-chat-completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ function extractFirstJSON(raw: string): string | null {
return null;
}

function repairInnerQuotes(json: string): string {
let result = json.replace(
/([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef])"([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef])/g,
'$1\\"$2'
);
result = result.replace(/([\u4e00-\u9fff\u3000-\u303f\uff00-\uffef])"(?=\s*[,}\]])/g, '$1\\"');
return result;
}

export class OpenAIChatCompletionProvider extends BaseAIProvider {
private readonly aiSessionManager: AISessionManager;

Expand Down Expand Up @@ -372,7 +381,18 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
return JSON.parse(raw);
} catch (e1) {
const fixed = extractFirstJSON(raw);
if (fixed) return JSON.parse(fixed);
if (fixed) {
try {
return JSON.parse(fixed);
} catch {
const repaired = repairInnerQuotes(fixed);
if (repaired !== fixed) {
try {
return JSON.parse(repaired);
} catch {}
}
}
}
throw e1;
}
})();
Expand Down
Loading
Loading