om-memory-system 3.4.0-next.26 → 3.4.2

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 (51) hide show
  1. package/dist/adapters/opencode/auto-capture-summary.js +1 -1
  2. package/dist/adapters/opencode/backfill-models.d.ts +1 -1
  3. package/dist/adapters/opencode/backfill-models.js +1 -1
  4. package/dist/adapters/opencode/backfill-startup.d.ts +2 -0
  5. package/dist/adapters/opencode/backfill-startup.js +15 -1
  6. package/dist/adapters/opencode/import-command.d.ts +3 -3
  7. package/dist/adapters/opencode/import-command.js +3 -3
  8. package/dist/adapters/opencode/internal-capture-sessions.d.ts +4 -0
  9. package/dist/{services/ai → adapters/opencode}/internal-capture-sessions.js +1 -14
  10. package/dist/{services/ai → adapters/opencode}/opencode-import-models.js +1 -1
  11. package/dist/{services/user-memory-learning.d.ts → adapters/opencode/profile-learning.d.ts} +1 -1
  12. package/dist/{services/user-memory-learning.js → adapters/opencode/profile-learning.js} +14 -14
  13. package/dist/{services/ai → adapters/opencode}/profile-llm-client.js +1 -1
  14. package/dist/adapters/opencode/profile-model.d.ts +23 -0
  15. package/dist/adapters/opencode/profile-model.js +117 -0
  16. package/dist/adapters/opencode/user-prompt.js +1 -1
  17. package/dist/core/profile-analysis.d.ts +3 -0
  18. package/dist/importer/backfill-controls.d.ts +16 -0
  19. package/dist/importer/backfill-controls.js +7 -0
  20. package/dist/importer/import-readiness.js +1 -1
  21. package/dist/{services/ai/internal-capture-sessions.d.ts → importer/opencode-internal-sessions.d.ts} +0 -3
  22. package/dist/importer/opencode-internal-sessions.js +14 -0
  23. package/dist/importer/opencode-reader.js +1 -1
  24. package/dist/importer/settings-health.js +8 -1
  25. package/dist/{services → importer}/settings-models.d.ts +4 -5
  26. package/dist/{services → importer}/settings-models.js +24 -13
  27. package/dist/importer/web-import-jobs.js +4 -5
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +13 -5
  30. package/dist/services/user-profile/ai-cleanup.d.ts +0 -16
  31. package/dist/services/user-profile/ai-cleanup.js +17 -91
  32. package/dist/services/user-profile/profile-model.d.ts +8 -0
  33. package/dist/services/user-profile/profile-model.js +31 -0
  34. package/dist/services/user-profile/user-profile-manager.d.ts +1 -1
  35. package/dist/services/user-profile/user-profile-manager.js +20 -74
  36. package/dist/services/web-server.js +1 -1
  37. package/dist/shared/api/schemas.d.ts +1 -1
  38. package/dist/v2/plugin.js +1 -1
  39. package/package.json +1 -1
  40. /package/dist/{services/ai → adapters/opencode}/opencode-diagnostics.d.ts +0 -0
  41. /package/dist/{services/ai → adapters/opencode}/opencode-diagnostics.js +0 -0
  42. /package/dist/{services/ai → adapters/opencode}/opencode-host-config.d.ts +0 -0
  43. /package/dist/{services/ai → adapters/opencode}/opencode-host-config.js +0 -0
  44. /package/dist/{services/ai → adapters/opencode}/opencode-import-models.d.ts +0 -0
  45. /package/dist/{services/ai → adapters/opencode}/opencode-provider-loader.d.ts +0 -0
  46. /package/dist/{services/ai → adapters/opencode}/opencode-provider-loader.js +0 -0
  47. /package/dist/{services/ai → adapters/opencode}/opencode-provider.d.ts +0 -0
  48. /package/dist/{services/ai → adapters/opencode}/opencode-provider.js +0 -0
  49. /package/dist/{services/ai → adapters/opencode}/opencode-sdk-client.d.ts +0 -0
  50. /package/dist/{services/ai → adapters/opencode}/opencode-sdk-client.js +0 -0
  51. /package/dist/{services/ai → adapters/opencode}/profile-llm-client.d.ts +0 -0
@@ -4,7 +4,7 @@ import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js
4
4
  import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
5
5
  import { classifyCaptureReply, normalizeStopReason, parseCaptureSummary, } from "../../core/extraction.js";
6
6
  import { log } from "../../services/logger.js";
7
- import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
7
+ import { loadOpencodeProvider } from "./opencode-provider-loader.js";
8
8
  function buildSystemPrompt(languageName) {
9
9
  return `You are a technical memory recorder for a software development project.
10
10
 
@@ -1,6 +1,6 @@
1
1
  import { selectImportModel } from "../../importer/model-selection.js";
2
2
  import type { HistoryImportModels } from "../../importer/run-import.js";
3
- import { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
3
+ import { createOpencodeImportModels } from "./opencode-import-models.js";
4
4
  interface BackfillConfig {
5
5
  opencodeBackfillModel?: string;
6
6
  opencodeProvider?: string;
@@ -3,7 +3,7 @@ import { parseBackfillModel } from "../../importer/backfill-model.js";
3
3
  import { resolveExternalBackfillModels } from "../../importer/external-backfill-models.js";
4
4
  import { selectImportModel } from "../../importer/model-selection.js";
5
5
  import { getAutoCaptureProviderStatus } from "../../services/ai/live-model-choice.js";
6
- import { createOpencodeImportModels, } from "../../services/ai/opencode-import-models.js";
6
+ import { createOpencodeImportModels } from "./opencode-import-models.js";
7
7
  /** Apply the live-model rule when inherit is selected; named models need sign-in. */
8
8
  export async function resolveOpencodeBackfillModels(config = CONFIG, context) {
9
9
  const choice = parseBackfillModel(config, "opencode");
@@ -11,3 +11,5 @@ export declare function registerOpencodeBackfillModels(input: {
11
11
  connected: string[];
12
12
  configModel: () => Promise<string | null>;
13
13
  }): void;
14
+ /** Let web imports, Health, and Settings in this process use OpenCode's signed-in models. */
15
+ export declare function registerOpencodeImportModels(): void;
@@ -1,7 +1,10 @@
1
1
  import { CONFIG } from "../../config.js";
2
2
  import { scheduleAutoBackfill } from "../../importer/auto-backfill.js";
3
- import { registerHostBackfillModels } from "../../importer/backfill-controls.js";
3
+ import { registerHostBackfillModels, registerOpencodeHostModels, } from "../../importer/backfill-controls.js";
4
+ import { listOpencodeClientModels } from "../../importer/settings-models.js";
4
5
  import { resolveOpencodeBackfillModels } from "./backfill-models.js";
6
+ import { createOpencodeImportModels } from "./opencode-import-models.js";
7
+ import { getV2Client, isProviderConnected } from "./opencode-provider.js";
5
8
  /** Start after connected providers load, without delaying plugin initialisation. */
6
9
  export function startOpencodeBackfill(input) {
7
10
  return scheduleAutoBackfill({
@@ -15,3 +18,14 @@ export function startOpencodeBackfill(input) {
15
18
  export function registerOpencodeBackfillModels(input) {
16
19
  registerHostBackfillModels("opencode", () => resolveOpencodeBackfillModels(CONFIG, input));
17
20
  }
21
+ /** Let web imports, Health, and Settings in this process use OpenCode's signed-in models. */
22
+ export function registerOpencodeImportModels() {
23
+ registerOpencodeHostModels({
24
+ isProviderConnected: (providerID) => isProviderConnected(providerID),
25
+ createImportModels: (ref, directory) => createOpencodeImportModels(ref, directory),
26
+ async listSettingsModels() {
27
+ const client = getV2Client();
28
+ return client ? listOpencodeClientModels(client) : null;
29
+ },
30
+ });
31
+ }
@@ -1,6 +1,6 @@
1
- import { type OpencodeModelRef } from "../../services/ai/opencode-import-models.js";
2
- export { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
3
- export type { OpencodeModelRef } from "../../services/ai/opencode-import-models.js";
1
+ import { type OpencodeModelRef } from "./opencode-import-models.js";
2
+ export { createOpencodeImportModels } from "./opencode-import-models.js";
3
+ export type { OpencodeModelRef } from "./opencode-import-models.js";
4
4
  /**
5
5
  * OpenCode command surface for the historical-session importer:
6
6
  *
@@ -1,9 +1,9 @@
1
1
  import { isConfigured } from "../../config.js";
2
2
  import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
3
3
  import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
4
- import { createOpencodeImportModels, } from "../../services/ai/opencode-import-models.js";
5
- export { createOpencodeImportModels } from "../../services/ai/opencode-import-models.js";
6
- import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
4
+ import { createOpencodeImportModels } from "./opencode-import-models.js";
5
+ export { createOpencodeImportModels } from "./opencode-import-models.js";
6
+ import { loadOpencodeProvider } from "./opencode-provider-loader.js";
7
7
  import { beginManualImport } from "../../importer/manual-import-guard.js";
8
8
  import { memoryClient } from "../../services/client.js";
9
9
  import { log } from "../../services/logger.js";
@@ -0,0 +1,4 @@
1
+ export { INTERNAL_CAPTURE_SESSION_TITLE, INTERNAL_CAPTURE_SESSION_TITLES, isInternalCaptureSessionTitle, } from "../../importer/opencode-internal-sessions.js";
2
+ export declare function trackInternalCaptureSession(sessionID: string): void;
3
+ export declare function untrackInternalCaptureSession(sessionID: string): void;
4
+ export declare function isTrackedInternalCaptureSession(sessionID: string): boolean;
@@ -1,21 +1,8 @@
1
- /** Title used for transient structured-output sessions (capture / profile learning). */
2
- export const INTERNAL_CAPTURE_SESSION_TITLE = "omms capture";
3
- /** Title used by opencode-mem builds; sessions left by older versions stay internal. */
4
- const LEGACY_INTERNAL_CAPTURE_SESSION_TITLE = "opencode-mem capture";
1
+ export { INTERNAL_CAPTURE_SESSION_TITLE, INTERNAL_CAPTURE_SESSION_TITLES, isInternalCaptureSessionTitle, } from "../../importer/opencode-internal-sessions.js";
5
2
  /** Grace period so session.idle can still match after best-effort delete. */
6
3
  const UNTRACK_GRACE_MS = 60_000;
7
4
  const trackedSessionIDs = new Set();
8
5
  const untrackTimers = new Map();
9
- /** Every title omms has used for its own sessions (capture, profile learning, profile cleanup). */
10
- export const INTERNAL_CAPTURE_SESSION_TITLES = [
11
- INTERNAL_CAPTURE_SESSION_TITLE,
12
- LEGACY_INTERNAL_CAPTURE_SESSION_TITLE,
13
- "omms profile cleanup",
14
- "opencode-mem profile cleanup",
15
- ];
16
- export function isInternalCaptureSessionTitle(title) {
17
- return (title === INTERNAL_CAPTURE_SESSION_TITLE || title === LEGACY_INTERNAL_CAPTURE_SESSION_TITLE);
18
- }
19
6
  export function trackInternalCaptureSession(sessionID) {
20
7
  const pending = untrackTimers.get(sessionID);
21
8
  if (pending) {
@@ -9,7 +9,7 @@ export async function createOpencodeImportModels(ref, directory) {
9
9
  throw new Error("the OpenCode client is not ready; retry in a moment");
10
10
  const { z } = await import("zod");
11
11
  const { buildCaptureSystemPrompt, createUserProfileAnalysisSchema, parseCaptureSummary } = await import("../../core/extraction.js");
12
- const { detectLanguage, getLanguageName } = await import("../language-detector.js");
12
+ const { detectLanguage, getLanguageName } = await import("../../services/language-detector.js");
13
13
  const captureSchema = z.object({
14
14
  summary: z.string().optional(),
15
15
  type: z.string(),
@@ -1,7 +1,7 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  export declare function shouldRunAutomaticProfileCleanup(previousPromptCount: number, addedPromptCount: number, interval?: number): boolean;
3
3
  export declare function performUserProfileLearning(ctx: PluginInput, directory: string): Promise<void>;
4
- import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX } from "../core/extraction.js";
4
+ import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX } from "../../core/extraction.js";
5
5
  export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
6
6
  export declare function createUserProfileToolSchema(existingProfile: boolean): {
7
7
  type: "function";
@@ -1,11 +1,11 @@
1
- import { getTags } from "./tags.js";
2
- import { log } from "./logger.js";
3
- import { CONFIG, refreshConfigIfChanged } from "../config.js";
4
- import { resolveOpencodeHostModel } from "./ai/live-model-choice.js";
5
- import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
6
- import { userProfileManager } from "./user-profile/user-profile-manager.js";
7
- import { sortProfileItems } from "../utils/profile.js";
8
- import { loadOpencodeProvider } from "./ai/opencode-provider-loader.js";
1
+ import { getTags } from "../../services/tags.js";
2
+ import { log } from "../../services/logger.js";
3
+ import { CONFIG, refreshConfigIfChanged } from "../../config.js";
4
+ import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
5
+ import { userPromptManager } from "../../services/user-prompt/user-prompt-manager.js";
6
+ import { userProfileManager } from "../../services/user-profile/user-profile-manager.js";
7
+ import { sortProfileItems } from "../../utils/profile.js";
8
+ import { loadOpencodeProvider } from "./opencode-provider-loader.js";
9
9
  let isLearningRunning = false;
10
10
  export function shouldRunAutomaticProfileCleanup(previousPromptCount, addedPromptCount, interval = CONFIG.userProfileAutoCleanupInterval) {
11
11
  if (!CONFIG.userProfileAutoCleanupEnabled || !Number.isInteger(interval) || interval <= 0) {
@@ -23,7 +23,7 @@ async function runAutomaticProfileCleanup(userId) {
23
23
  const itemCount = profileData.preferences.length + profileData.patterns.length + profileData.workflows.length;
24
24
  if (itemCount < 2)
25
25
  return;
26
- const { aiCleanupProfile } = await import("./user-profile/ai-cleanup.js");
26
+ const { aiCleanupProfile } = await import("../../services/user-profile/ai-cleanup.js");
27
27
  const result = await aiCleanupProfile(profileData);
28
28
  if (result.diff.merged.length === 0 && result.diff.removed.length === 0)
29
29
  return;
@@ -338,7 +338,7 @@ CRITICAL: Only output observations grounded in the RECENT PROMPTS above. Write d
338
338
  }
339
339
  return truncate(base);
340
340
  }
341
- import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX, } from "../core/extraction.js";
341
+ import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX, } from "../../core/extraction.js";
342
342
  export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
343
343
  export function createUserProfileToolSchema(existingProfile) {
344
344
  return {
@@ -497,7 +497,7 @@ async function analyzeUserProfile(context, existingProfile) {
497
497
  log("user-profile-learning: trying opencode provider");
498
498
  try {
499
499
  const { generateStructuredOutput } = await loadOpencodeProvider();
500
- const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
500
+ const { getOpenCodeClient } = await import("./profile-llm-client.js");
501
501
  log("user-profile-learning: opencode provider diag", {
502
502
  provider: resolveOpencodeHostModel(CONFIG)?.providerID,
503
503
  model: resolveOpencodeHostModel(CONFIG)?.modelID,
@@ -556,8 +556,8 @@ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "ne
556
556
  });
557
557
  throw new Error("External API not configured for user memory learning");
558
558
  }
559
- const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
560
- const { buildMemoryProviderConfig } = await import("./ai/provider-config.js");
559
+ const { AIProviderFactory } = await import("../../services/ai/ai-provider-factory.js");
560
+ const { buildMemoryProviderConfig } = await import("../../services/ai/provider-config.js");
561
561
  const providerConfig = buildMemoryProviderConfig(CONFIG);
562
562
  const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
563
563
  const systemPrompt = `You are a user behavior analyst for a coding assistant.
@@ -597,7 +597,7 @@ If no clear chains, return { "paths": [] }.`;
597
597
  try {
598
598
  const { z } = await import("zod");
599
599
  const { generateStructuredOutput } = await loadOpencodeProvider();
600
- const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
600
+ const { getOpenCodeClient } = await import("./profile-llm-client.js");
601
601
  let v2Client;
602
602
  try {
603
603
  v2Client = await getOpenCodeClient();
@@ -1,5 +1,5 @@
1
1
  import { CONFIG } from "../../config.js";
2
- import { resolveOpencodeHostModel } from "./live-model-choice.js";
2
+ import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
3
3
  import { loadOpencodeProvider } from "./opencode-provider-loader.js";
4
4
  let _cachedClient = null;
5
5
  let _cachedProvider = null;
@@ -0,0 +1,23 @@
1
+ import type { ModelPort } from "../../core/profile-analysis.js";
2
+ import type { OpencodeModelRef } from "./opencode-import-models.js";
3
+ type PromptInfo = {
4
+ error?: {
5
+ name: string;
6
+ data?: {
7
+ message?: string;
8
+ };
9
+ };
10
+ };
11
+ /**
12
+ * Extract assistant text from an OpenCode session.prompt result.
13
+ * AssistantMessage has no `text` field; content lives in `parts` (#177).
14
+ */
15
+ export declare function extractTextFromPromptResult(promptResult: unknown): {
16
+ info: PromptInfo | undefined;
17
+ rawText: string;
18
+ };
19
+ /** Adapt OpenCode's host model to the shared profile code, as `adaptPiProfileModel` does for Pi. */
20
+ export declare function adaptOpencodeProfileModel(v2Client: unknown, hostModel: OpencodeModelRef): ModelPort;
21
+ /** Route shared profile calls in this process to OpenCode's host model while one is configured. */
22
+ export declare function registerOpencodeProfileModel(): void;
23
+ export {};
@@ -0,0 +1,117 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
3
+ import { log } from "../../services/logger.js";
4
+ import { OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS } from "../../services/request-timeouts.js";
5
+ import { registerHostProfileModel } from "../../services/user-profile/profile-model.js";
6
+ import { loadOpencodeProvider } from "./opencode-provider-loader.js";
7
+ import { getOpenCodeClient } from "./profile-llm-client.js";
8
+ /**
9
+ * Extract assistant text from an OpenCode session.prompt result.
10
+ * AssistantMessage has no `text` field; content lives in `parts` (#177).
11
+ */
12
+ export function extractTextFromPromptResult(promptResult) {
13
+ const result = promptResult;
14
+ const info = result?.data?.info ?? result?.info;
15
+ const parts = result?.data?.parts ?? result?.parts ?? [];
16
+ const rawText = parts
17
+ .filter((p) => p.type === "text" && p.text)
18
+ .map((p) => p.text)
19
+ .join("\n")
20
+ .trim();
21
+ return { info, rawText };
22
+ }
23
+ function raceWithTimeout(promise, ms, message) {
24
+ let timer;
25
+ const timeout = new Promise((_, reject) => {
26
+ timer = setTimeout(() => reject(new Error(message)), ms);
27
+ });
28
+ return Promise.race([promise, timeout]).finally(() => {
29
+ if (timer !== undefined)
30
+ clearTimeout(timer);
31
+ });
32
+ }
33
+ /**
34
+ * Plain-text reply through a transient OpenCode session. Only AI profile cleanup
35
+ * calls it, so the session carries the cleanup title the history reader skips.
36
+ */
37
+ async function completeViaSession(v2Client, hostModel, systemPrompt, userPrompt) {
38
+ const t0 = Date.now();
39
+ // Only "inherit" (or no model configured) needs resolving to the session's recent model.
40
+ const model = hostModel.modelID === "inherit"
41
+ ? (await loadOpencodeProvider()).resolveOpencodeModelRef(hostModel)
42
+ : hostModel;
43
+ const created = (await raceWithTimeout(v2Client.session.create({
44
+ title: "omms profile cleanup",
45
+ directory: process.cwd(),
46
+ }), 30000, "session.create timeout"));
47
+ log("AI cleanup: session.create result", {
48
+ rawType: typeof created,
49
+ keys: Object.keys(created || {}),
50
+ hasData: !!created?.data,
51
+ dataId: created?.data?.id,
52
+ });
53
+ const sessionID = created?.data?.id || created?.id || created?.sessionID;
54
+ if (!sessionID)
55
+ throw new Error("session.create returned no session id");
56
+ log("AI cleanup: session created", { sessionID, createMs: Date.now() - t0 });
57
+ try {
58
+ const TIMEOUT_MS = OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS;
59
+ const promptResult = await raceWithTimeout(v2Client.session.prompt({
60
+ sessionID,
61
+ model,
62
+ system: systemPrompt,
63
+ parts: [{ type: "text", text: userPrompt }],
64
+ // `noReply` suppresses assistant generation; cleanup needs the JSON reply (#177).
65
+ noReply: false,
66
+ }), TIMEOUT_MS, `opencodeClient prompt timeout after ${TIMEOUT_MS}ms`);
67
+ log("AI cleanup: session.prompt done", { promptMs: Date.now() - t0 });
68
+ const { info, rawText } = extractTextFromPromptResult(promptResult);
69
+ if (!info)
70
+ throw new Error("prompt response missing info");
71
+ if (info.error)
72
+ throw new Error(`opencode reported ${info.error.name}: ${info.error.data?.message ?? ""}`);
73
+ return rawText;
74
+ }
75
+ finally {
76
+ try {
77
+ await v2Client.session.delete({ sessionID });
78
+ }
79
+ catch {
80
+ // ignore cleanup failures for ephemeral sessions
81
+ }
82
+ }
83
+ }
84
+ /** Adapt OpenCode's host model to the shared profile code, as `adaptPiProfileModel` does for Pi. */
85
+ export function adaptOpencodeProfileModel(v2Client, hostModel) {
86
+ return {
87
+ provider: hostModel.providerID,
88
+ modelId: hostModel.modelID,
89
+ complete: (systemPrompt, userPrompt) => completeViaSession(v2Client, hostModel, systemPrompt, userPrompt),
90
+ async completeStructured(systemPrompt, userPrompt, schema) {
91
+ const client = await getOpenCodeClient();
92
+ const { generateStructuredOutput } = await loadOpencodeProvider();
93
+ return generateStructuredOutput({
94
+ client,
95
+ providerID: hostModel.providerID,
96
+ modelID: hostModel.modelID,
97
+ systemPrompt,
98
+ userPrompt,
99
+ schema,
100
+ });
101
+ },
102
+ };
103
+ }
104
+ /** Route shared profile calls in this process to OpenCode's host model while one is configured. */
105
+ export function registerOpencodeProfileModel() {
106
+ registerHostProfileModel(async () => {
107
+ const hostModel = resolveOpencodeHostModel(CONFIG);
108
+ if (!hostModel)
109
+ return null;
110
+ const v2Client = (await loadOpencodeProvider()).getV2Client();
111
+ if (!v2Client) {
112
+ log("profile model: opencode client unavailable, falling back to external API");
113
+ return null;
114
+ }
115
+ return adaptOpencodeProfileModel(v2Client, hostModel);
116
+ });
117
+ }
@@ -1,5 +1,5 @@
1
1
  import { isStructuredSummaryPromptMessage } from "../../core/internal-prompt.js";
2
- import { isInternalStructuredSession } from "../../services/ai/opencode-provider.js";
2
+ import { isInternalStructuredSession } from "./opencode-provider.js";
3
3
  import { userPromptManager } from "../../services/user-prompt/user-prompt-manager.js";
4
4
  export { isStructuredSummaryPromptMessage };
5
5
  /** True when a prompt is omms's own internal traffic and must not be recorded or searched. */
@@ -1,8 +1,11 @@
1
+ import { z } from "zod";
1
2
  import type { UserProfileData } from "../services/user-profile/types.js";
2
3
  export interface ModelPort {
3
4
  provider: string;
4
5
  modelId: string;
5
6
  complete(systemPrompt: string, userPrompt: string): Promise<string>;
7
+ /** Host-enforced JSON output; callers fall back to `complete` and parse when absent. */
8
+ completeStructured?<T>(systemPrompt: string, userPrompt: string, schema: z.ZodType<T>): Promise<T>;
6
9
  }
7
10
  export interface ExistingProfileInput {
8
11
  id: string;
@@ -6,6 +6,22 @@ type ResolveModels = () => Promise<{
6
6
  models: HistoryImportModels;
7
7
  }>;
8
8
  export declare function registerHostBackfillModels(host: BackfillHost, resolve: ResolveModels): void;
9
+ export interface SettingsModel {
10
+ provider: string;
11
+ model: string;
12
+ name: string;
13
+ }
14
+ /** OpenCode's model access for web imports, Health, and Settings, when OpenCode runs this process. */
15
+ export interface OpencodeHostModels {
16
+ isProviderConnected(providerID: string): boolean;
17
+ createImportModels(ref: {
18
+ providerID: string;
19
+ modelID: string;
20
+ }, directory: string): Promise<Required<HistoryImportModels>>;
21
+ listSettingsModels(): Promise<SettingsModel[] | null>;
22
+ }
23
+ export declare function registerOpencodeHostModels(models: OpencodeHostModels | null): void;
24
+ export declare function getOpencodeHostModels(): OpencodeHostModels | null;
9
25
  /** A control request the page cannot carry out; `status` is the HTTP status. */
10
26
  export declare class BackfillControlError extends Error {
11
27
  readonly status: number;
@@ -8,6 +8,13 @@ const hostResolvers = new Map();
8
8
  export function registerHostBackfillModels(host, resolve) {
9
9
  hostResolvers.set(host, resolve);
10
10
  }
11
+ let opencodeHostModels = null;
12
+ export function registerOpencodeHostModels(models) {
13
+ opencodeHostModels = models;
14
+ }
15
+ export function getOpencodeHostModels() {
16
+ return opencodeHostModels;
17
+ }
11
18
  /** A control request the page cannot carry out; `status` is the HTTP status. */
12
19
  export class BackfillControlError extends Error {
13
20
  status;
@@ -24,7 +24,7 @@ export async function importReadiness(deps = {}) {
24
24
  const [state, opencode, piReader] = await Promise.all([
25
25
  externalApiState(deps.supportedProviders),
26
26
  (deps.listOpencodeModels ??
27
- (async () => (await import("../services/settings-models.js")).listOpencodeSettingsModels()))(),
27
+ (async () => (await import("./settings-models.js")).listOpencodeSettingsModels()))(),
28
28
  (deps.loadPiSdk ?? (() => import("@earendil-works/pi-coding-agent")))().then(() => ({ available: true }), () => ({
29
29
  available: false,
30
30
  reason: "The Pi SDK (@earendil-works/pi-coding-agent) is not installed for OpenCode",
@@ -3,6 +3,3 @@ export declare const INTERNAL_CAPTURE_SESSION_TITLE = "omms capture";
3
3
  /** Every title omms has used for its own sessions (capture, profile learning, profile cleanup). */
4
4
  export declare const INTERNAL_CAPTURE_SESSION_TITLES: readonly ["omms capture", "opencode-mem capture", "omms profile cleanup", "opencode-mem profile cleanup"];
5
5
  export declare function isInternalCaptureSessionTitle(title: string | undefined | null): boolean;
6
- export declare function trackInternalCaptureSession(sessionID: string): void;
7
- export declare function untrackInternalCaptureSession(sessionID: string): void;
8
- export declare function isTrackedInternalCaptureSession(sessionID: string): boolean;
@@ -0,0 +1,14 @@
1
+ /** Title used for transient structured-output sessions (capture / profile learning). */
2
+ export const INTERNAL_CAPTURE_SESSION_TITLE = "omms capture";
3
+ /** Title used by opencode-mem builds; sessions left by older versions stay internal. */
4
+ const LEGACY_INTERNAL_CAPTURE_SESSION_TITLE = "opencode-mem capture";
5
+ /** Every title omms has used for its own sessions (capture, profile learning, profile cleanup). */
6
+ export const INTERNAL_CAPTURE_SESSION_TITLES = [
7
+ INTERNAL_CAPTURE_SESSION_TITLE,
8
+ LEGACY_INTERNAL_CAPTURE_SESSION_TITLE,
9
+ "omms profile cleanup",
10
+ "opencode-mem profile cleanup",
11
+ ];
12
+ export function isInternalCaptureSessionTitle(title) {
13
+ return (title === INTERNAL_CAPTURE_SESSION_TITLE || title === LEGACY_INTERNAL_CAPTURE_SESSION_TITLE);
14
+ }
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { DatabaseSync } from "node:sqlite";
6
- import { INTERNAL_CAPTURE_SESSION_TITLES } from "../services/ai/internal-capture-sessions.js";
6
+ import { INTERNAL_CAPTURE_SESSION_TITLES } from "./opencode-internal-sessions.js";
7
7
  import { acquirePrivateSnapshot, opencodeSnapshots, OpencodeSnapshotError, } from "./opencode-snapshot.js";
8
8
  export const DEFAULT_OPENCODE_DB = join(homedir(), ".local/share/opencode/opencode.db");
9
9
  function validateSchema(db) {
@@ -4,6 +4,13 @@ import { isLoopbackHost } from "../services/web-api-auth.js";
4
4
  import { safeHealthError } from "../services/safe-health-error.js";
5
5
  export { safeHealthError } from "../services/safe-health-error.js";
6
6
  const PROBE = "Reply with a short acknowledgement.";
7
+ async function createOpencodeImportModels(ref, directory) {
8
+ const { getOpencodeHostModels } = await import("./backfill-controls.js");
9
+ const opencode = getOpencodeHostModels();
10
+ if (!opencode)
11
+ throw new Error("the OpenCode client is not ready; retry in a moment");
12
+ return opencode.createImportModels(ref, directory);
13
+ }
7
14
  export function captureFailureHealth(byModel, byReason) {
8
15
  const total = byModel.reduce((count, row) => count + row.total, 0);
9
16
  const failed = byModel.reduce((count, row) => count + row.failed, 0);
@@ -93,7 +100,7 @@ export async function runSettingsHealth(input) {
93
100
  if (ref?.modelID === "inherit")
94
101
  throw new Error("A session model needs an active session to test");
95
102
  const capture = ref
96
- ? (await (await import("../services/ai/opencode-import-models.js")).createOpencodeImportModels(ref, input.directory)).capture
103
+ ? (await createOpencodeImportModels(ref, input.directory)).capture
97
104
  : (await import("./model-selection.js")).selectImportModel({}).capture;
98
105
  await capture.summarize({
99
106
  userPrompt: PROBE,
@@ -1,15 +1,14 @@
1
1
  import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
2
+ import type { SettingsModel } from "./backfill-controls.js";
3
+ /** Connected models from an OpenCode client; null when the client returns no list. */
4
+ export declare function listOpencodeClientModels(client: Pick<OpencodeClient, "provider">): Promise<SettingsModel[] | null>;
2
5
  export declare function listOpencodeSettingsModels(client?: Pick<OpencodeClient, "provider">): Promise<{
3
6
  available: false;
4
7
  reason: string;
5
8
  models?: undefined;
6
9
  } | {
7
10
  available: true;
8
- models: {
9
- provider: string;
10
- model: string;
11
- name: string;
12
- }[];
11
+ models: SettingsModel[];
13
12
  reason?: undefined;
14
13
  }>;
15
14
  export declare function listPiSettingsModels(loadSdk?: () => Promise<typeof import("@earendil-works/pi-coding-agent")>): Promise<{
@@ -1,19 +1,30 @@
1
+ /** Connected models from an OpenCode client; null when the client returns no list. */
2
+ export async function listOpencodeClientModels(client) {
3
+ const result = await client.provider.list();
4
+ if (!result.data)
5
+ return null;
6
+ const connected = new Set(result.data.connected);
7
+ return result.data.all
8
+ .filter((provider) => connected.has(provider.id))
9
+ .flatMap((provider) => Object.entries(provider.models).map(([model, info]) => ({
10
+ provider: provider.id,
11
+ model,
12
+ name: info.name,
13
+ })));
14
+ }
1
15
  export async function listOpencodeSettingsModels(client) {
2
16
  try {
3
- const active = client ?? (await import("./ai/opencode-provider.js")).getV2Client();
4
- if (!active)
5
- return { available: false, reason: "OpenCode model list unavailable" };
6
- const result = await active.provider.list();
7
- if (!result.data)
17
+ let models;
18
+ if (client) {
19
+ models = await listOpencodeClientModels(client);
20
+ }
21
+ else {
22
+ const { getOpencodeHostModels } = await import("./backfill-controls.js");
23
+ const opencode = getOpencodeHostModels();
24
+ models = opencode ? await opencode.listSettingsModels() : null;
25
+ }
26
+ if (!models)
8
27
  return { available: false, reason: "OpenCode model list unavailable" };
9
- const connected = new Set(result.data.connected);
10
- const models = result.data.all
11
- .filter((provider) => connected.has(provider.id))
12
- .flatMap((provider) => Object.entries(provider.models).map(([model, info]) => ({
13
- provider: provider.id,
14
- model,
15
- name: info.name,
16
- })));
17
28
  return { available: true, models };
18
29
  }
19
30
  catch {
@@ -173,12 +173,11 @@ export class SettingsImportJobs {
173
173
  }
174
174
  else {
175
175
  const [providerID = "", ...id] = input.modelChoice.split("/");
176
- const { loadOpencodeProvider } = await import("../services/ai/opencode-provider-loader.js");
177
- const { isProviderConnected } = await loadOpencodeProvider();
178
- if (!isProviderConnected(providerID))
176
+ const { getOpencodeHostModels } = await import("./backfill-controls.js");
177
+ const opencode = getOpencodeHostModels();
178
+ if (!opencode?.isProviderConnected(providerID))
179
179
  throw new Error("OpenCode provider is not connected");
180
- const { createOpencodeImportModels } = await import("../services/ai/opencode-import-models.js");
181
- models = await createOpencodeImportModels({ providerID, modelID: id.join("/") }, directory);
180
+ models = await opencode.createImportModels({ providerID, modelID: id.join("/") }, directory);
182
181
  }
183
182
  const { memoryClient } = await import("../services/client.js");
184
183
  await memoryClient.warmup();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
  import { isStructuredSummaryPromptMessage } from "./adapters/opencode/user-prompt.js";
3
- import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle } from "./services/ai/internal-capture-sessions.js";
3
+ import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle } from "./adapters/opencode/internal-capture-sessions.js";
4
4
  export { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle };
5
5
  export { isStructuredSummaryPromptMessage };
6
6
  /**
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { isInternalPrompt, isStructuredSummaryPromptMessage, recordUserPrompt, }
8
8
  import { executeMemoryOperation } from "./core/memory-operations.js";
9
9
  import { OPENCODE_IMPORT_COMMAND, OPENCODE_IMPORT_DESCRIPTION, latestUserMessageModel, runOpencodeImportCommand, } from "./adapters/opencode/import-command.js";
10
10
  import { formatMemoriesForCompaction } from "./core/retrieval.js";
11
- import { performUserProfileLearning } from "./services/user-memory-learning.js";
11
+ import { performUserProfileLearning } from "./adapters/opencode/profile-learning.js";
12
12
  import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
13
13
  import { startWebServer, WebServer } from "./services/web-server.js";
14
14
  import { pruneTraces } from "./services/capture-diagnostics.js";
@@ -19,10 +19,11 @@ import { isConfigured, CONFIG, initConfigWithLegacyMigration } from "./config.js
19
19
  import { resolveOpencodeHostModel } from "./services/ai/live-model-choice.js";
20
20
  import { log } from "./services/logger.js";
21
21
  import { getLanguageName } from "./services/language-detector.js";
22
- import { getHostClientConfig } from "./services/ai/opencode-host-config.js";
23
- import { loadOpencodeProvider } from "./services/ai/opencode-provider-loader.js";
24
- import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./services/ai/opencode-provider.js";
25
- import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./services/ai/internal-capture-sessions.js";
22
+ import { getHostClientConfig } from "./adapters/opencode/opencode-host-config.js";
23
+ import { loadOpencodeProvider } from "./adapters/opencode/opencode-provider-loader.js";
24
+ import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./adapters/opencode/opencode-provider.js";
25
+ import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./adapters/opencode/internal-capture-sessions.js";
26
+ import { registerOpencodeProfileModel } from "./adapters/opencode/profile-model.js";
26
27
  export { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle };
27
28
  export { isStructuredSummaryPromptMessage };
28
29
  function extractSessionTitle(response) {
@@ -209,6 +210,13 @@ export const OmmsPlugin = async (ctx) => {
209
210
  })();
210
211
  }
211
212
  await configureOpencodeHostTransport(ctx);
213
+ // Shared profile dedup, conflict, description, and cleanup calls use OpenCode's host model.
214
+ registerOpencodeProfileModel();
215
+ // Web imports, Health, and Settings served by this process can list and use OpenCode's models.
216
+ // Awaited so the web server never serves those routes before the registry is filled.
217
+ await import("./adapters/opencode/backfill-startup.js")
218
+ .then(({ registerOpencodeImportModels }) => registerOpencodeImportModels())
219
+ .catch(() => { });
212
220
  (async () => {
213
221
  let connected = [];
214
222
  try {
@@ -35,22 +35,6 @@ interface AIMapping {
35
35
  merged: string[][];
36
36
  removed: string[];
37
37
  }
38
- type PromptInfo = {
39
- error?: {
40
- name: string;
41
- data?: {
42
- message?: string;
43
- };
44
- };
45
- };
46
- /**
47
- * Extract assistant text from an OpenCode session.prompt result.
48
- * AssistantMessage has no `text` field; content lives in `parts` (#177).
49
- */
50
- export declare function extractTextFromPromptResult(promptResult: unknown): {
51
- info: PromptInfo | undefined;
52
- rawText: string;
53
- };
54
38
  export declare function rebuildProfileUsing(mapping: AIMapping, cleanedById: Map<string, IndexedProfileItem>, originalById: Map<string, IndexedProfileItem>, counters?: {
55
39
  cleaned: number;
56
40
  original: number;
@@ -1,8 +1,7 @@
1
1
  import { CONFIG } from "../../config.js";
2
- import { resolveOpencodeHostModel } from "../ai/live-model-choice.js";
3
2
  import { log } from "../logger.js";
4
- import { loadOpencodeProvider } from "../ai/opencode-provider-loader.js";
5
- import { EXTERNAL_PROFILE_CLEANUP_TIMEOUT_MS, OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS, } from "../request-timeouts.js";
3
+ import { resolveHostProfileModel } from "./profile-model.js";
4
+ import { EXTERNAL_PROFILE_CLEANUP_TIMEOUT_MS } from "../request-timeouts.js";
6
5
  import { applySafeExtraParams } from "../ai/providers/base-provider.js";
7
6
  export async function aiCleanupProfile(profileData) {
8
7
  const t0 = Date.now();
@@ -149,16 +148,12 @@ function formatForAI(item) {
149
148
  return { id, category, description, frequency };
150
149
  }
151
150
  async function callAICleanup(prompt) {
152
- // Use opencode internal session when opencodeProvider is configured (same pattern as auto-capture).
153
- // When the client is available, surface OpenCode errors instead of masking them as
151
+ // Use the host model when the host registered one (same pattern as auto-capture).
152
+ // When it is available, surface its errors instead of masking them as
154
153
  // "No AI provider configured" via a silent fallback (#177).
155
- if (resolveOpencodeHostModel(CONFIG)) {
156
- const { getV2Client } = await loadOpencodeProvider();
157
- const v2Client = getV2Client();
158
- if (v2Client) {
159
- return callViaOpencodeWithClient(v2Client, prompt);
160
- }
161
- log("AI cleanup: opencode client unavailable, falling back to external API");
154
+ const hostModel = await resolveHostProfileModel();
155
+ if (hostModel) {
156
+ return callViaHostModel(hostModel, prompt);
162
157
  }
163
158
  if (CONFIG.memoryModel && CONFIG.memoryApiUrl) {
164
159
  return callViaExternalAPI(prompt);
@@ -208,86 +203,17 @@ async function callViaExternalAPI(prompt) {
208
203
  mapping: normalizeAIMapping(parsed.mapping),
209
204
  };
210
205
  }
211
- /**
212
- * Extract assistant text from an OpenCode session.prompt result.
213
- * AssistantMessage has no `text` field; content lives in `parts` (#177).
214
- */
215
- export function extractTextFromPromptResult(promptResult) {
216
- const result = promptResult;
217
- const info = result?.data?.info ?? result?.info;
218
- const parts = result?.data?.parts ?? result?.parts ?? [];
219
- const rawText = parts
220
- .filter((p) => p.type === "text" && p.text)
221
- .map((p) => p.text)
222
- .join("\n")
223
- .trim();
224
- return { info, rawText };
225
- }
226
- function raceWithTimeout(promise, ms, message) {
227
- let timer;
228
- const timeout = new Promise((_, reject) => {
229
- timer = setTimeout(() => reject(new Error(message)), ms);
230
- });
231
- return Promise.race([promise, timeout]).finally(() => {
232
- if (timer !== undefined)
233
- clearTimeout(timer);
234
- });
235
- }
236
- async function callViaOpencodeWithClient(v2Client, prompt) {
237
- const t0 = Date.now();
238
- const hostModel = resolveOpencodeHostModel(CONFIG) ?? { providerID: "", modelID: "inherit" };
239
- // Only "inherit" (or no model configured) needs resolving to the session's recent model.
240
- const model = hostModel.modelID === "inherit"
241
- ? (await loadOpencodeProvider()).resolveOpencodeModelRef(hostModel)
242
- : hostModel;
206
+ async function callViaHostModel(model, prompt) {
243
207
  const systemPrompt = "You are a user profile cleanup assistant. Merge duplicate entries and return only JSON without markdown wrapping.";
244
- const created = (await raceWithTimeout(v2Client.session.create({
245
- title: "omms profile cleanup",
246
- directory: process.cwd(),
247
- }), 30000, "session.create timeout"));
248
- log("AI cleanup: session.create result", {
249
- rawType: typeof created,
250
- keys: Object.keys(created || {}),
251
- hasData: !!created?.data,
252
- dataId: created?.data?.id,
253
- });
254
- const sessionID = created?.data?.id || created?.id || created?.sessionID;
255
- if (!sessionID)
256
- throw new Error("session.create returned no session id");
257
- log("AI cleanup: session created", { sessionID, createMs: Date.now() - t0 });
258
- try {
259
- const TIMEOUT_MS = OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS;
260
- const promptResult = await raceWithTimeout(v2Client.session.prompt({
261
- sessionID,
262
- model,
263
- system: systemPrompt,
264
- parts: [{ type: "text", text: prompt }],
265
- // `noReply` suppresses assistant generation; cleanup needs the JSON reply (#177).
266
- noReply: false,
267
- }), TIMEOUT_MS, `opencodeClient prompt timeout after ${TIMEOUT_MS}ms`);
268
- log("AI cleanup: session.prompt done", { promptMs: Date.now() - t0 });
269
- const { info, rawText } = extractTextFromPromptResult(promptResult);
270
- if (!info)
271
- throw new Error("prompt response missing info");
272
- if (info.error)
273
- throw new Error(`opencode reported ${info.error.name}: ${info.error.data?.message ?? ""}`);
274
- const jsonMatch = rawText.match(/\{[\s\S]*\}/);
275
- if (!jsonMatch)
276
- throw new Error("AI response did not contain valid JSON");
277
- const parsed = JSON.parse(jsonMatch[0]);
278
- return {
279
- profile: parsed,
280
- mapping: normalizeAIMapping(parsed.mapping),
281
- };
282
- }
283
- finally {
284
- try {
285
- await v2Client.session.delete({ sessionID });
286
- }
287
- catch {
288
- // ignore cleanup failures for ephemeral sessions
289
- }
290
- }
208
+ const rawText = await model.complete(systemPrompt, prompt);
209
+ const jsonMatch = rawText.match(/\{[\s\S]*\}/);
210
+ if (!jsonMatch)
211
+ throw new Error("AI response did not contain valid JSON");
212
+ const parsed = JSON.parse(jsonMatch[0]);
213
+ return {
214
+ profile: parsed,
215
+ mapping: normalizeAIMapping(parsed.mapping),
216
+ };
291
217
  }
292
218
  function buildItemIndex(profile) {
293
219
  const map = new Map();
@@ -0,0 +1,8 @@
1
+ import type { z } from "zod";
2
+ import type { ModelPort } from "../../core/profile-analysis.js";
3
+ type ResolveProfileModel = () => Promise<ModelPort | null>;
4
+ export declare function registerHostProfileModel(resolve: ResolveProfileModel | null): void;
5
+ /** A host model that fails to resolve counts as unavailable, so callers keep their external API fallback. */
6
+ export declare function resolveHostProfileModel(): Promise<ModelPort | null>;
7
+ export declare function completeStructured<T>(model: ModelPort, systemPrompt: string, userPrompt: string, schema: z.ZodType<T>): Promise<T>;
8
+ export {};
@@ -0,0 +1,31 @@
1
+ import { extractJsonObject } from "../../core/extraction.js";
2
+ import { log } from "../logger.js";
3
+ /**
4
+ * The host model for profile dedup, conflict, description, and cleanup calls in
5
+ * this process. A host registers it at start-up; with none, or when it resolves
6
+ * to null, those calls use the external API.
7
+ */
8
+ let hostProfileModel = null;
9
+ export function registerHostProfileModel(resolve) {
10
+ hostProfileModel = resolve;
11
+ }
12
+ /** A host model that fails to resolve counts as unavailable, so callers keep their external API fallback. */
13
+ export async function resolveHostProfileModel() {
14
+ if (!hostProfileModel)
15
+ return null;
16
+ try {
17
+ return await hostProfileModel();
18
+ }
19
+ catch (error) {
20
+ log("profile model: host model unavailable, falling back to external API", {
21
+ error: String(error),
22
+ });
23
+ return null;
24
+ }
25
+ }
26
+ export async function completeStructured(model, systemPrompt, userPrompt, schema) {
27
+ if (model.completeStructured) {
28
+ return model.completeStructured(systemPrompt, userPrompt, schema);
29
+ }
30
+ return schema.parse(extractJsonObject(await model.complete(systemPrompt, userPrompt)));
31
+ }
@@ -59,7 +59,7 @@ export declare class UserProfileManager {
59
59
  private checkConflict;
60
60
  private evolveDescription;
61
61
  evolveAndUpdate(item: any, itemType: string, profileId?: string): Promise<void>;
62
- private callOpencodeProvider;
62
+ private callHostModel;
63
63
  private callExternalAPI;
64
64
  }
65
65
  export declare const userProfileManager: UserProfileManager;
@@ -2,12 +2,11 @@ import { join } from "node:path";
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { tursoConnectionManager } from "../turso/connection-manager.js";
4
4
  import { CONFIG } from "../../config.js";
5
- import { resolveOpencodeHostModel } from "../ai/live-model-choice.js";
5
+ import { completeStructured, resolveHostProfileModel } from "./profile-model.js";
6
6
  import { safeArray } from "./profile-utils.js";
7
7
  import { EmbeddingService } from "../embedding.js";
8
8
  import { log } from "../logger.js";
9
9
  import { cosineSimilarityNumbers, l2Normalize } from "../../utils/math.js";
10
- import { loadOpencodeProvider } from "../ai/opencode-provider-loader.js";
11
10
  const CENTROID_EMA_WEIGHT = 0.85;
12
11
  const CENTROID_EMA_WEIGHT_COMPLEMENT = 0.15;
13
12
  const THREE_WAY_CENTROID_W1 = 0.45;
@@ -1179,32 +1178,15 @@ A: "${descA}"
1179
1178
  B: "${descB}"
1180
1179
 
1181
1180
  Answer JSON only: { "duplicate": true|false, "reason": "one sentence explanation" }`;
1182
- if (resolveOpencodeHostModel(CONFIG)) {
1181
+ const hostModel = await resolveHostProfileModel();
1182
+ if (hostModel) {
1183
1183
  try {
1184
1184
  const { z } = await import("zod");
1185
- const { generateStructuredOutput } = await loadOpencodeProvider();
1186
- const { getOpenCodeClient } = await import("../ai/profile-llm-client.js");
1187
- let v2Client;
1188
- try {
1189
- v2Client = await getOpenCodeClient();
1190
- }
1191
- catch (e) {
1192
- log("profile dedup check: native provider not connected", { error: String(e) });
1193
- }
1194
- if (v2Client) {
1195
- const result = await Promise.race([
1196
- generateStructuredOutput({
1197
- client: v2Client,
1198
- providerID: resolveOpencodeHostModel(CONFIG).providerID,
1199
- modelID: resolveOpencodeHostModel(CONFIG).modelID,
1200
- systemPrompt: "You are a semantic duplicate detector. Output valid JSON.",
1201
- userPrompt: prompt,
1202
- schema: z.object({ duplicate: z.boolean(), reason: z.string() }),
1203
- }),
1204
- new Promise((_, reject) => setTimeout(() => reject(new Error("dedup check timeout")), 30000)),
1205
- ]);
1206
- return result.duplicate || false;
1207
- }
1185
+ const result = await Promise.race([
1186
+ completeStructured(hostModel, "You are a semantic duplicate detector. Output valid JSON.", prompt, z.object({ duplicate: z.boolean(), reason: z.string() })),
1187
+ new Promise((_, reject) => setTimeout(() => reject(new Error("dedup check timeout")), 30000)),
1188
+ ]);
1189
+ return result.duplicate || false;
1208
1190
  }
1209
1191
  catch (e) {
1210
1192
  log("profile dedup check: native provider failed", { error: String(e) });
@@ -1366,32 +1348,15 @@ A: "${descA}"
1366
1348
  B: "${descB}"
1367
1349
 
1368
1350
  Answer JSON only: { "conflict": true|false, "reason": "one sentence explanation" }`;
1369
- if (resolveOpencodeHostModel(CONFIG)) {
1351
+ const hostModel = await resolveHostProfileModel();
1352
+ if (hostModel) {
1370
1353
  try {
1371
1354
  const { z } = await import("zod");
1372
- const { generateStructuredOutput } = await loadOpencodeProvider();
1373
- const { getOpenCodeClient } = await import("../ai/profile-llm-client.js");
1374
- let v2Client;
1375
- try {
1376
- v2Client = await getOpenCodeClient();
1377
- }
1378
- catch (e) {
1379
- log("profile conflict check: native provider not connected", { error: String(e) });
1380
- }
1381
- if (v2Client) {
1382
- const result = await Promise.race([
1383
- generateStructuredOutput({
1384
- client: v2Client,
1385
- providerID: resolveOpencodeHostModel(CONFIG).providerID,
1386
- modelID: resolveOpencodeHostModel(CONFIG).modelID,
1387
- systemPrompt: "You are a preference contradiction detector. Output valid JSON.",
1388
- userPrompt: prompt,
1389
- schema: z.object({ conflict: z.boolean(), reason: z.string() }),
1390
- }),
1391
- new Promise((_, reject) => setTimeout(() => reject(new Error("conflict check timeout")), 30000)),
1392
- ]);
1393
- return result.conflict || false;
1394
- }
1355
+ const result = await Promise.race([
1356
+ completeStructured(hostModel, "You are a preference contradiction detector. Output valid JSON.", prompt, z.object({ conflict: z.boolean(), reason: z.string() })),
1357
+ new Promise((_, reject) => setTimeout(() => reject(new Error("conflict check timeout")), 30000)),
1358
+ ]);
1359
+ return result.conflict || false;
1395
1360
  }
1396
1361
  catch (e) {
1397
1362
  log("profile conflict check: native provider failed", { error: String(e) });
@@ -1482,9 +1447,10 @@ ${evidenceList}
1482
1447
 
1483
1448
  Generate a concise, abstract description of the user's general behavioral tendency.`;
1484
1449
  let newDescription = null;
1485
- if (resolveOpencodeHostModel(CONFIG)) {
1450
+ const hostModel = await resolveHostProfileModel();
1451
+ if (hostModel) {
1486
1452
  try {
1487
- newDescription = await this.callOpencodeProvider(systemPrompt, userPrompt);
1453
+ newDescription = await this.callHostModel(hostModel, systemPrompt, userPrompt);
1488
1454
  }
1489
1455
  catch (e) {
1490
1456
  log("profile description evolution: native provider failed, trying external API", {
@@ -1594,31 +1560,11 @@ Generate a concise, abstract description of the user's general behavioral tenden
1594
1560
  log("profile description evolve error", { type: itemType, error: String(e) });
1595
1561
  }
1596
1562
  }
1597
- async callOpencodeProvider(systemPrompt, userPrompt) {
1598
- const { generateStructuredOutput } = await loadOpencodeProvider();
1599
- const { getOpenCodeClient } = await import("../ai/profile-llm-client.js");
1600
- let v2Client;
1601
- try {
1602
- v2Client = await getOpenCodeClient();
1603
- }
1604
- catch (e) {
1605
- log("profile description evolution: native provider not connected", {
1606
- provider: resolveOpencodeHostModel(CONFIG)?.providerID,
1607
- error: String(e),
1608
- });
1609
- return null;
1610
- }
1563
+ async callHostModel(model, systemPrompt, userPrompt) {
1611
1564
  const { z } = await import("zod");
1612
1565
  const schema = z.object({ description: z.string() });
1613
1566
  const result = await Promise.race([
1614
- generateStructuredOutput({
1615
- client: v2Client,
1616
- providerID: resolveOpencodeHostModel(CONFIG).providerID,
1617
- modelID: resolveOpencodeHostModel(CONFIG).modelID,
1618
- systemPrompt,
1619
- userPrompt,
1620
- schema,
1621
- }),
1567
+ completeStructured(model, systemPrompt, userPrompt, schema),
1622
1568
  new Promise((_, reject) => setTimeout(() => reject(new Error("evolve description timeout")), 120000)),
1623
1569
  ]);
1624
1570
  return result.description || null;
@@ -533,7 +533,7 @@ export class WebServer {
533
533
  return this.jsonResponse(await directoryMapsView());
534
534
  }
535
535
  if (path === "/api/settings/models" && method === "GET") {
536
- const { listOpencodeSettingsModels, listPiSettingsModels } = await import("./settings-models.js");
536
+ const { listOpencodeSettingsModels, listPiSettingsModels } = await import("../importer/settings-models.js");
537
537
  const host = url.searchParams.get("host");
538
538
  if (host === "pi")
539
539
  return this.jsonResponse(await listPiSettingsModels());
@@ -259,8 +259,8 @@ export declare const ApplyCleanupRequestSchema: z.ZodObject<{
259
259
  }, z.core.$strip>;
260
260
  export declare const UpdateProfileItemRequestSchema: z.ZodObject<{
261
261
  type: z.ZodEnum<{
262
- preferences: "preferences";
263
262
  patterns: "patterns";
263
+ preferences: "preferences";
264
264
  workflows: "workflows";
265
265
  }>;
266
266
  index: z.ZodNumber;
package/dist/v2/plugin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OmmsPlugin } from "../index.js";
2
- import { loadOpencodeProvider } from "../services/ai/opencode-provider-loader.js";
2
+ import { loadOpencodeProvider } from "../adapters/opencode/opencode-provider-loader.js";
3
3
  import { registerV2Adapter } from "./adapter.js";
4
4
  import { createV2MemoryBridge } from "./memory-bridge.js";
5
5
  import { createLegacyClient } from "./legacy-client.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "om-memory-system",
3
- "version": "3.4.0-next.26",
3
+ "version": "3.4.2",
4
4
  "description": "Opinionated modular memory system for coding agents (OpenCode + Pi)",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",