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.
- package/dist/adapters/opencode/auto-capture-summary.js +1 -1
- package/dist/adapters/opencode/backfill-models.d.ts +1 -1
- package/dist/adapters/opencode/backfill-models.js +1 -1
- package/dist/adapters/opencode/backfill-startup.d.ts +2 -0
- package/dist/adapters/opencode/backfill-startup.js +15 -1
- package/dist/adapters/opencode/import-command.d.ts +3 -3
- package/dist/adapters/opencode/import-command.js +3 -3
- package/dist/adapters/opencode/internal-capture-sessions.d.ts +4 -0
- package/dist/{services/ai → adapters/opencode}/internal-capture-sessions.js +1 -14
- package/dist/{services/ai → adapters/opencode}/opencode-import-models.js +1 -1
- package/dist/{services/user-memory-learning.d.ts → adapters/opencode/profile-learning.d.ts} +1 -1
- package/dist/{services/user-memory-learning.js → adapters/opencode/profile-learning.js} +14 -14
- package/dist/{services/ai → adapters/opencode}/profile-llm-client.js +1 -1
- package/dist/adapters/opencode/profile-model.d.ts +23 -0
- package/dist/adapters/opencode/profile-model.js +117 -0
- package/dist/adapters/opencode/user-prompt.js +1 -1
- package/dist/core/profile-analysis.d.ts +3 -0
- package/dist/importer/backfill-controls.d.ts +16 -0
- package/dist/importer/backfill-controls.js +7 -0
- package/dist/importer/import-readiness.js +1 -1
- package/dist/{services/ai/internal-capture-sessions.d.ts → importer/opencode-internal-sessions.d.ts} +0 -3
- package/dist/importer/opencode-internal-sessions.js +14 -0
- package/dist/importer/opencode-reader.js +1 -1
- package/dist/importer/settings-health.js +8 -1
- package/dist/{services → importer}/settings-models.d.ts +4 -5
- package/dist/{services → importer}/settings-models.js +24 -13
- package/dist/importer/web-import-jobs.js +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -5
- package/dist/services/user-profile/ai-cleanup.d.ts +0 -16
- package/dist/services/user-profile/ai-cleanup.js +17 -91
- package/dist/services/user-profile/profile-model.d.ts +8 -0
- package/dist/services/user-profile/profile-model.js +31 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +1 -1
- package/dist/services/user-profile/user-profile-manager.js +20 -74
- package/dist/services/web-server.js +1 -1
- package/dist/shared/api/schemas.d.ts +1 -1
- package/dist/v2/plugin.js +1 -1
- package/package.json +1 -1
- /package/dist/{services/ai → adapters/opencode}/opencode-diagnostics.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-diagnostics.js +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-host-config.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-host-config.js +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-import-models.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-provider-loader.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-provider-loader.js +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-provider.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-provider.js +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-sdk-client.d.ts +0 -0
- /package/dist/{services/ai → adapters/opencode}/opencode-sdk-client.js +0 -0
- /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 "
|
|
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 "
|
|
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
|
|
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 "
|
|
2
|
-
export { createOpencodeImportModels } from "
|
|
3
|
-
export type { OpencodeModelRef } from "
|
|
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
|
|
5
|
-
export { createOpencodeImportModels } from "
|
|
6
|
-
import { loadOpencodeProvider } from "
|
|
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
|
-
|
|
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("
|
|
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 "
|
|
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 "
|
|
2
|
-
import { log } from "
|
|
3
|
-
import { CONFIG, refreshConfigIfChanged } from "
|
|
4
|
-
import { resolveOpencodeHostModel } from "
|
|
5
|
-
import { userPromptManager } from "
|
|
6
|
-
import { userProfileManager } from "
|
|
7
|
-
import { sortProfileItems } from "
|
|
8
|
-
import { loadOpencodeProvider } from "./
|
|
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("
|
|
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 "
|
|
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("./
|
|
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("
|
|
560
|
-
const { buildMemoryProviderConfig } = await import("
|
|
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("./
|
|
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 "
|
|
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 "
|
|
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("
|
|
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",
|
package/dist/{services/ai/internal-capture-sessions.d.ts → importer/opencode-internal-sessions.d.ts}
RENAMED
|
@@ -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 "
|
|
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
|
|
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
|
-
|
|
4
|
-
if (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
177
|
-
const
|
|
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
|
-
|
|
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 "./
|
|
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 "./
|
|
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 "./
|
|
23
|
-
import { loadOpencodeProvider } from "./
|
|
24
|
-
import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./
|
|
25
|
-
import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./
|
|
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 {
|
|
5
|
-
import { EXTERNAL_PROFILE_CLEANUP_TIMEOUT_MS
|
|
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
|
|
153
|
-
// When
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1181
|
+
const hostModel = await resolveHostProfileModel();
|
|
1182
|
+
if (hostModel) {
|
|
1183
1183
|
try {
|
|
1184
1184
|
const { z } = await import("zod");
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1351
|
+
const hostModel = await resolveHostProfileModel();
|
|
1352
|
+
if (hostModel) {
|
|
1370
1353
|
try {
|
|
1371
1354
|
const { z } = await import("zod");
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
1450
|
+
const hostModel = await resolveHostProfileModel();
|
|
1451
|
+
if (hostModel) {
|
|
1486
1452
|
try {
|
|
1487
|
-
newDescription = await this.
|
|
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
|
|
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
|
-
|
|
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("
|
|
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 "../
|
|
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
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|