om-memory-system 3.7.0-next.51 → 3.7.0-next.52
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/package-skills.d.ts +16 -0
- package/dist/adapters/opencode/package-skills.js +60 -0
- package/dist/adapters/opencode/profile-learning.js +9 -2
- package/dist/adapters/pi/extension.js +5 -1
- package/dist/adapters/pi/profile.js +7 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/profile-catch-up-command.d.ts +2 -0
- package/dist/cli/profile-catch-up-command.js +110 -0
- package/dist/cli/web-command.js +3 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -4
- package/dist/core/memory-tool-text.d.ts +4 -0
- package/dist/core/memory-tool-text.js +6 -0
- package/dist/core/profile-backoff.d.ts +20 -0
- package/dist/core/profile-backoff.js +41 -0
- package/dist/core/profile-failure.d.ts +16 -0
- package/dist/core/profile-failure.js +34 -0
- package/dist/core/trivial-prompt.d.ts +5 -0
- package/dist/core/trivial-prompt.js +8 -0
- package/dist/importer/claude-hook-api.js +13 -2
- package/dist/importer/model-selection.d.ts +2 -0
- package/dist/importer/model-selection.js +15 -4
- package/dist/importer/profile-backlog.d.ts +42 -0
- package/dist/importer/profile-backlog.js +79 -0
- package/dist/importer/profile-catch-up.d.ts +39 -0
- package/dist/importer/profile-catch-up.js +123 -0
- package/dist/importer/profile-import.d.ts +1 -1
- package/dist/importer/profile-import.js +14 -35
- package/dist/importer/settings-health.js +50 -11
- package/dist/importer/web-import-api.d.ts +1 -0
- package/dist/importer/web-import-api.js +1 -0
- package/dist/index.js +4 -1
- package/dist/services/api-tokens.d.ts +35 -0
- package/dist/services/api-tokens.js +185 -0
- package/dist/services/capture-attempt-store.d.ts +3 -1
- package/dist/services/capture-attempt-store.js +12 -5
- package/dist/services/context.js +4 -1
- package/dist/services/embedding-change.d.ts +52 -0
- package/dist/services/embedding-change.js +177 -0
- package/dist/services/embedding.d.ts +23 -1
- package/dist/services/embedding.js +63 -24
- package/dist/services/global-config-writer.d.ts +12 -1
- package/dist/services/global-config-writer.js +32 -4
- package/dist/services/migration-service.js +4 -1
- package/dist/services/settings-snapshot.d.ts +8 -0
- package/dist/services/settings-snapshot.js +11 -0
- package/dist/services/user-prompt/profile-catch-up-lease.d.ts +34 -0
- package/dist/services/user-prompt/profile-catch-up-lease.js +77 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +14 -1
- package/dist/services/user-prompt/user-prompt-manager.js +36 -1
- package/dist/services/web-api-auth.d.ts +6 -2
- package/dist/services/web-api-auth.js +11 -9
- package/dist/services/web-password.d.ts +11 -0
- package/dist/services/web-password.js +30 -0
- package/dist/services/web-server.d.ts +8 -0
- package/dist/services/web-server.js +182 -19
- package/dist/v2/adapter.js +3 -0
- package/dist/web/assets/index-B6LyHM22.css +2 -0
- package/dist/web/assets/index-CVvXO3zL.js +125 -0
- package/dist/web/index.html +2 -2
- package/package.json +5 -1
- package/skills/omms-memory/SKILL.md +93 -0
- package/dist/web/assets/index-Bh5aFNPX.js +0 -124
- package/dist/web/assets/index-DG7RjW5B.css +0 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A prompt too short to say anything about the user, such as "yes go". The
|
|
3
|
+
* word rule keeps short real preferences such as "use bun not npm".
|
|
4
|
+
*/
|
|
5
|
+
export function isTrivialPrompt(text) {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
return trimmed.length < 20 && trimmed.split(/\s+/).filter(Boolean).length < 3;
|
|
8
|
+
}
|
|
@@ -408,6 +408,13 @@ async function learnProfile(directory, deps) {
|
|
|
408
408
|
const interval = CONFIG.userProfileAnalysisInterval ?? 0;
|
|
409
409
|
if (!(interval > 0) || learning)
|
|
410
410
|
return;
|
|
411
|
+
const { profileBackoff, profileCatchUpLock, recordProfileFailure } = await import("../core/profile-backoff.js");
|
|
412
|
+
// A catch-up run, here or in a terminal, analyses the same prompts; skip the live pass meanwhile.
|
|
413
|
+
if (!profileBackoff.canRun() || profileCatchUpLock.isActive())
|
|
414
|
+
return;
|
|
415
|
+
const { CatchUpLease } = await import("../services/user-prompt/profile-catch-up-lease.js");
|
|
416
|
+
if (await new CatchUpLease().isActive().catch(() => false))
|
|
417
|
+
return;
|
|
411
418
|
learning = true;
|
|
412
419
|
try {
|
|
413
420
|
const [{ userPromptManager }, { userProfileManager }, { analyzeProfile }] = await Promise.all([
|
|
@@ -415,6 +422,7 @@ async function learnProfile(directory, deps) {
|
|
|
415
422
|
import("../services/user-profile/user-profile-manager.js"),
|
|
416
423
|
import("../core/profile-analysis.js"),
|
|
417
424
|
]);
|
|
425
|
+
await userPromptManager.skipTrivialPromptsForLearning();
|
|
418
426
|
if ((await userPromptManager.countUnanalyzedForUserLearning()) < interval)
|
|
419
427
|
return;
|
|
420
428
|
const user = getTags(directory).user;
|
|
@@ -422,7 +430,9 @@ async function learnProfile(directory, deps) {
|
|
|
422
430
|
log("Claude Code profile learning skipped", { code: "no-user-email" });
|
|
423
431
|
return;
|
|
424
432
|
}
|
|
425
|
-
const batch = await userPromptManager.getPromptsForUserLearning(interval
|
|
433
|
+
const batch = await userPromptManager.getPromptsForUserLearning(interval, {
|
|
434
|
+
recentFirst: true,
|
|
435
|
+
});
|
|
426
436
|
if (batch.length === 0)
|
|
427
437
|
return;
|
|
428
438
|
const model = deps.profileModel?.() ?? (await externalModels()).profile;
|
|
@@ -439,13 +449,14 @@ async function learnProfile(directory, deps) {
|
|
|
439
449
|
await userProfileManager.createProfile(user.userEmail, user.displayName || user.userEmail, user.userName || user.userEmail, user.userEmail, analysis.raw, batch.length);
|
|
440
450
|
}
|
|
441
451
|
await userPromptManager.markMultipleAsUserLearningCaptured(batch.map((prompt) => prompt.id));
|
|
452
|
+
profileBackoff.recordSuccess();
|
|
442
453
|
log("Claude Code profile learning finished", {
|
|
443
454
|
prompts: batch.length,
|
|
444
455
|
hadExisting: Boolean(existing),
|
|
445
456
|
});
|
|
446
457
|
}
|
|
447
458
|
catch (error) {
|
|
448
|
-
|
|
459
|
+
recordProfileFailure("claude-code", "Claude Code profile learning failed", error);
|
|
449
460
|
}
|
|
450
461
|
finally {
|
|
451
462
|
learning = false;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CaptureSummaryProvider } from "../core/host.js";
|
|
2
2
|
import type { ModelPort } from "../core/profile-analysis.js";
|
|
3
|
+
/** The same limit the Pi and OpenCode host-model profile paths use. */
|
|
4
|
+
export declare const PROFILE_REQUEST_TIMEOUT_MS = 120000;
|
|
3
5
|
export interface ImportModelFlags {
|
|
4
6
|
provider?: string;
|
|
5
7
|
model?: string;
|
|
@@ -3,10 +3,13 @@ import { CONFIG } from "../config.js";
|
|
|
3
3
|
import { buildBoundedSummaryPrompt } from "../core/capture-context.js";
|
|
4
4
|
import { httpStatusError } from "../core/capture-retry-policy.js";
|
|
5
5
|
import { buildCaptureSystemPrompt, captureSummaryToolSchema, normalizeStopReason, parseCaptureSummary, } from "../core/extraction.js";
|
|
6
|
+
import { ProfileModelError, profileResultCode } from "../core/profile-failure.js";
|
|
6
7
|
import { AIProviderFactory } from "../services/ai/ai-provider-factory.js";
|
|
7
8
|
import { toolCallFailureReason } from "../services/ai/providers/base-provider.js";
|
|
8
9
|
import { buildMemoryProviderConfig } from "../services/ai/provider-config.js";
|
|
9
10
|
import { resolveSecretValue } from "../services/secret-resolver.js";
|
|
11
|
+
/** The same limit the Pi and OpenCode host-model profile paths use. */
|
|
12
|
+
export const PROFILE_REQUEST_TIMEOUT_MS = 120_000;
|
|
10
13
|
/** Merge import-only model settings without changing the saved omms config. */
|
|
11
14
|
export function selectImportModel(flags) {
|
|
12
15
|
const providerName = flags.provider ?? CONFIG.memoryProvider;
|
|
@@ -29,15 +32,23 @@ export function selectImportModel(flags) {
|
|
|
29
32
|
if (!AIProviderFactory.getSupportedProviders().includes(providerName)) {
|
|
30
33
|
throw new Error(`Unsupported import provider: ${providerName}`);
|
|
31
34
|
}
|
|
32
|
-
const
|
|
35
|
+
const settings = {
|
|
33
36
|
...CONFIG,
|
|
34
37
|
memoryProvider: providerName,
|
|
35
38
|
memoryModel: modelId,
|
|
36
39
|
memoryApiUrl: apiUrl,
|
|
37
40
|
memoryApiKey: key,
|
|
38
|
-
}
|
|
41
|
+
};
|
|
42
|
+
const provider = AIProviderFactory.createProvider(providerName, buildMemoryProviderConfig(settings));
|
|
43
|
+
// A profile reply can take over a minute; captures keep autoCaptureIterationTimeout.
|
|
44
|
+
const profileProvider = AIProviderFactory.createProvider(providerName, buildMemoryProviderConfig(settings, { iterationTimeout: PROFILE_REQUEST_TIMEOUT_MS }));
|
|
39
45
|
const capture = {
|
|
40
46
|
async summarize(request) {
|
|
47
|
+
if (request.diagnostics) {
|
|
48
|
+
request.diagnostics.path = "external-api";
|
|
49
|
+
request.diagnostics.provider = providerName;
|
|
50
|
+
request.diagnostics.model = modelId ?? "";
|
|
51
|
+
}
|
|
41
52
|
const { detectLanguage, getLanguageName } = await import("../services/language-detector.js");
|
|
42
53
|
const target = CONFIG.autoCaptureLanguage && CONFIG.autoCaptureLanguage !== "auto"
|
|
43
54
|
? CONFIG.autoCaptureLanguage
|
|
@@ -84,9 +95,9 @@ export function selectImportModel(flags) {
|
|
|
84
95
|
parameters: z.toJSONSchema(createUserProfileAnalysisSchema(z)),
|
|
85
96
|
},
|
|
86
97
|
};
|
|
87
|
-
const result = await
|
|
98
|
+
const result = await profileProvider.executeToolCall(system, prompt, tool, `history-profile-${randomUUID()}`);
|
|
88
99
|
if (!result.success || !result.data)
|
|
89
|
-
throw new
|
|
100
|
+
throw new ProfileModelError(profileResultCode(result));
|
|
90
101
|
return typeof result.data === "string" ? result.data : JSON.stringify(result.data);
|
|
91
102
|
},
|
|
92
103
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ModelPort } from "../core/profile-analysis.js";
|
|
2
|
+
import { type ProfileFailureCode } from "../core/profile-failure.js";
|
|
3
|
+
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
4
|
+
import { userPromptManager } from "../services/user-prompt/user-prompt-manager.js";
|
|
5
|
+
export type BacklogPromptStore = Pick<typeof userPromptManager, "countUnanalyzedForUserLearning" | "getPromptsForUserLearning" | "markMultipleAsUserLearningCaptured"> & Partial<Pick<typeof userPromptManager, "skipTrivialPromptsForLearning">>;
|
|
6
|
+
export type BacklogProfileStore = Pick<typeof userProfileManager, "getActiveProfile" | "createProfile" | "updateProfile">;
|
|
7
|
+
export interface DrainProfileBacklogOptions {
|
|
8
|
+
user: {
|
|
9
|
+
userEmail: string;
|
|
10
|
+
displayName?: string;
|
|
11
|
+
userName?: string;
|
|
12
|
+
};
|
|
13
|
+
model: ModelPort;
|
|
14
|
+
batchSize?: number;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
/** Names the profile change, for example "History import". */
|
|
17
|
+
label?: string;
|
|
18
|
+
promptStore?: BacklogPromptStore;
|
|
19
|
+
profileStore?: BacklogProfileStore;
|
|
20
|
+
onProgress?: (progress: {
|
|
21
|
+
batchesBuilt: number;
|
|
22
|
+
remaining: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
/** Called before each batch; a run that no longer owns the catch-up record stops. */
|
|
25
|
+
beforeBatch?: () => Promise<"ok" | "superseded" | "aborted">;
|
|
26
|
+
/** Called after each batch, whatever its outcome. */
|
|
27
|
+
afterBatch?: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface DrainProfileBacklogReport {
|
|
30
|
+
batchesBuilt: number;
|
|
31
|
+
remaining: number;
|
|
32
|
+
error?: string;
|
|
33
|
+
reason?: ProfileFailureCode;
|
|
34
|
+
/** A newer catch-up run took over before the next batch. */
|
|
35
|
+
superseded?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Analyse every waiting prompt, oldest first, in batches. Each finished batch
|
|
39
|
+
* stays learned; a batch that fails twice stops the run with its reason code,
|
|
40
|
+
* so the next run continues from the prompts still waiting.
|
|
41
|
+
*/
|
|
42
|
+
export declare function drainProfileBacklog(options: DrainProfileBacklogOptions): Promise<DrainProfileBacklogReport>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { analyzeProfile } from "../core/profile-analysis.js";
|
|
2
|
+
import { profileFailureCode } from "../core/profile-failure.js";
|
|
3
|
+
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
4
|
+
import { userPromptManager } from "../services/user-prompt/user-prompt-manager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Analyse every waiting prompt, oldest first, in batches. Each finished batch
|
|
7
|
+
* stays learned; a batch that fails twice stops the run with its reason code,
|
|
8
|
+
* so the next run continues from the prompts still waiting.
|
|
9
|
+
*/
|
|
10
|
+
export async function drainProfileBacklog(options) {
|
|
11
|
+
const batchSize = options.batchSize ?? 50;
|
|
12
|
+
const prompts = options.promptStore ?? userPromptManager;
|
|
13
|
+
const profiles = options.profileStore ?? userProfileManager;
|
|
14
|
+
const label = options.label ?? "Profile catch-up";
|
|
15
|
+
const { user, model } = options;
|
|
16
|
+
const report = { batchesBuilt: 0, remaining: 0 };
|
|
17
|
+
await prompts.skipTrivialPromptsForLearning?.();
|
|
18
|
+
while ((report.remaining = await prompts.countUnanalyzedForUserLearning()) > 0) {
|
|
19
|
+
if (options.signal?.aborted)
|
|
20
|
+
break;
|
|
21
|
+
const start = (await options.beforeBatch?.()) ?? "ok";
|
|
22
|
+
if (start === "superseded") {
|
|
23
|
+
report.superseded = true;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (start === "aborted")
|
|
27
|
+
break;
|
|
28
|
+
let outcome;
|
|
29
|
+
try {
|
|
30
|
+
outcome = await sendBatch();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await options.afterBatch?.();
|
|
34
|
+
}
|
|
35
|
+
if (outcome === "failed")
|
|
36
|
+
return report;
|
|
37
|
+
if (outcome === "empty")
|
|
38
|
+
break;
|
|
39
|
+
options.onProgress?.({
|
|
40
|
+
batchesBuilt: report.batchesBuilt,
|
|
41
|
+
remaining: await prompts.countUnanalyzedForUserLearning(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return report;
|
|
45
|
+
/** One batch with one retry; "failed" stops the run with its reason code. */
|
|
46
|
+
async function sendBatch() {
|
|
47
|
+
const batch = await prompts.getPromptsForUserLearning(batchSize);
|
|
48
|
+
if (batch.length === 0)
|
|
49
|
+
return "empty";
|
|
50
|
+
let succeeded = false;
|
|
51
|
+
for (let attempt = 0; attempt < 2 && !succeeded; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
const existing = await profiles.getActiveProfile(user.userEmail);
|
|
54
|
+
const context = batch.map((item, index) => `${index + 1}. ${item.content}`).join("\n");
|
|
55
|
+
const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
56
|
+
if (existing) {
|
|
57
|
+
if (!analysis.merged ||
|
|
58
|
+
!(await profiles.updateProfile(existing.id, analysis.merged, batch.length, `${label} of ${batch.length} prompts`)))
|
|
59
|
+
throw new Error("Profile update conflict");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await profiles.createProfile(user.userEmail, user.displayName || user.userEmail, user.userName || user.userEmail, user.userEmail, analysis.raw, batch.length);
|
|
63
|
+
}
|
|
64
|
+
await prompts.markMultipleAsUserLearningCaptured(batch.map((item) => item.id));
|
|
65
|
+
report.batchesBuilt++;
|
|
66
|
+
succeeded = true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (attempt === 1) {
|
|
70
|
+
report.error = error instanceof Error ? error.message : String(error);
|
|
71
|
+
report.reason = profileFailureCode(error);
|
|
72
|
+
report.remaining = await prompts.countUnanalyzedForUserLearning();
|
|
73
|
+
return "failed";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return "sent";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ModelPort } from "../core/profile-analysis.js";
|
|
2
|
+
import type { ProfileFailureCode } from "../core/profile-failure.js";
|
|
3
|
+
import { CatchUpLease } from "../services/user-prompt/profile-catch-up-lease.js";
|
|
4
|
+
export declare const CATCH_UP_BATCH_SIZE = 50;
|
|
5
|
+
export interface CatchUpState {
|
|
6
|
+
/** "superseded": a newer run, from the page or the terminal, took over. */
|
|
7
|
+
state: "idle" | "running" | "paused" | "done" | "failed" | "superseded";
|
|
8
|
+
batchesBuilt: number;
|
|
9
|
+
remaining: number;
|
|
10
|
+
reason?: ProfileFailureCode;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Batch hooks that keep catch-up runs in different processes apart: the
|
|
14
|
+
* newest run owns the record, and waits for the older run's current batch.
|
|
15
|
+
*/
|
|
16
|
+
export declare function catchUpLeaseHooks(signal?: AbortSignal, lease?: CatchUpLease): Promise<{
|
|
17
|
+
beforeBatch: () => Promise<import("../services/user-prompt/profile-catch-up-lease.js").BatchStart>;
|
|
18
|
+
afterBatch: () => Promise<void>;
|
|
19
|
+
release: () => Promise<void>;
|
|
20
|
+
}>;
|
|
21
|
+
export declare class CatchUpError extends Error {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
constructor(message: string, status: number, options?: {
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** The waiting prompts and the model calls a run would make. */
|
|
28
|
+
export declare function previewCatchUp(): Promise<{
|
|
29
|
+
waiting: number;
|
|
30
|
+
calls: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function catchUpState(): CatchUpState;
|
|
33
|
+
/**
|
|
34
|
+
* Start a catch-up run in the background with the saved external API, or
|
|
35
|
+
* another model. Pause aborts between batches; Resume starts a new run that
|
|
36
|
+
* continues from the prompts still waiting.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startCatchUp(directory: string, model?: ModelPort, onEnd?: (state: CatchUpState) => void): Promise<CatchUpState>;
|
|
39
|
+
export declare function pauseCatchUp(): CatchUpState;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { profileCatchUpLock } from "../core/profile-backoff.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { log } from "../services/logger.js";
|
|
4
|
+
import { CatchUpLease } from "../services/user-prompt/profile-catch-up-lease.js";
|
|
5
|
+
import { getTags } from "../services/tags.js";
|
|
6
|
+
import { drainProfileBacklog } from "./profile-backlog.js";
|
|
7
|
+
export const CATCH_UP_BATCH_SIZE = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Batch hooks that keep catch-up runs in different processes apart: the
|
|
10
|
+
* newest run owns the record, and waits for the older run's current batch.
|
|
11
|
+
*/
|
|
12
|
+
export async function catchUpLeaseHooks(signal, lease = new CatchUpLease()) {
|
|
13
|
+
const owner = `${process.pid}-${randomUUID()}`;
|
|
14
|
+
await lease.take(owner);
|
|
15
|
+
return {
|
|
16
|
+
beforeBatch: () => lease.beginBatch(owner, { signal }),
|
|
17
|
+
afterBatch: () => lease.endBatch(owner),
|
|
18
|
+
release: () => lease.release(owner),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export class CatchUpError extends Error {
|
|
22
|
+
status;
|
|
23
|
+
constructor(message, status, options) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.name = "CatchUpError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The waiting prompts and the model calls a run would make. */
|
|
30
|
+
export async function previewCatchUp() {
|
|
31
|
+
const { userPromptManager } = await import("../services/user-prompt/user-prompt-manager.js");
|
|
32
|
+
// Trivial prompts are skipped without a model call, so they are not counted.
|
|
33
|
+
const waiting = (await userPromptManager.countUnanalyzedForUserLearning()) -
|
|
34
|
+
(await userPromptManager.countTrivialPromptsForLearning());
|
|
35
|
+
return { waiting, calls: Math.ceil(waiting / CATCH_UP_BATCH_SIZE) };
|
|
36
|
+
}
|
|
37
|
+
let job = { state: "idle", batchesBuilt: 0, remaining: 0 };
|
|
38
|
+
let controller = null;
|
|
39
|
+
export function catchUpState() {
|
|
40
|
+
return job;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start a catch-up run in the background with the saved external API, or
|
|
44
|
+
* another model. Pause aborts between batches; Resume starts a new run that
|
|
45
|
+
* continues from the prompts still waiting.
|
|
46
|
+
*/
|
|
47
|
+
export async function startCatchUp(directory, model, onEnd) {
|
|
48
|
+
const user = getTags(directory).user;
|
|
49
|
+
if (!user.userEmail)
|
|
50
|
+
throw new CatchUpError("Profile learning needs a user email", 400);
|
|
51
|
+
if (!profileCatchUpLock.begin())
|
|
52
|
+
throw new CatchUpError("A catch-up run is already running", 409);
|
|
53
|
+
let chosen;
|
|
54
|
+
try {
|
|
55
|
+
chosen = model ?? (await import("./model-selection.js")).selectImportModel({}).profile;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
profileCatchUpLock.end();
|
|
59
|
+
throw new CatchUpError(error instanceof Error ? error.message : "No model", 400);
|
|
60
|
+
}
|
|
61
|
+
const runController = new AbortController();
|
|
62
|
+
const signal = runController.signal;
|
|
63
|
+
let remaining;
|
|
64
|
+
let hooks;
|
|
65
|
+
try {
|
|
66
|
+
remaining = (await previewCatchUp()).waiting;
|
|
67
|
+
hooks = await catchUpLeaseHooks(signal);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Setup failed before the run started; free the lock so a retry can start.
|
|
71
|
+
profileCatchUpLock.end();
|
|
72
|
+
throw new CatchUpError(error instanceof Error ? error.message : "Catch-up not started", 500, {
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
controller = runController;
|
|
77
|
+
job = { state: "running", batchesBuilt: 0, remaining };
|
|
78
|
+
void drainProfileBacklog({
|
|
79
|
+
beforeBatch: hooks.beforeBatch,
|
|
80
|
+
afterBatch: hooks.afterBatch,
|
|
81
|
+
user: { ...user, userEmail: user.userEmail },
|
|
82
|
+
model: chosen,
|
|
83
|
+
batchSize: CATCH_UP_BATCH_SIZE,
|
|
84
|
+
signal,
|
|
85
|
+
onProgress: (progress) => {
|
|
86
|
+
job = { ...job, ...progress };
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
.then((report) => {
|
|
90
|
+
job = {
|
|
91
|
+
state: report.reason
|
|
92
|
+
? "failed"
|
|
93
|
+
: report.superseded
|
|
94
|
+
? "superseded"
|
|
95
|
+
: signal.aborted
|
|
96
|
+
? "paused"
|
|
97
|
+
: "done",
|
|
98
|
+
batchesBuilt: report.batchesBuilt,
|
|
99
|
+
remaining: report.remaining,
|
|
100
|
+
...(report.reason ? { reason: report.reason } : {}),
|
|
101
|
+
};
|
|
102
|
+
})
|
|
103
|
+
.catch(() => {
|
|
104
|
+
job = { ...job, state: "failed", reason: "error" };
|
|
105
|
+
})
|
|
106
|
+
.finally(async () => {
|
|
107
|
+
await hooks.release().catch(() => { });
|
|
108
|
+
profileCatchUpLock.end();
|
|
109
|
+
controller = null;
|
|
110
|
+
log("Profile catch-up ended", {
|
|
111
|
+
outcome: job.state,
|
|
112
|
+
batches: job.batchesBuilt,
|
|
113
|
+
remaining: job.remaining,
|
|
114
|
+
...(job.reason ? { reason: job.reason } : {}),
|
|
115
|
+
});
|
|
116
|
+
onEnd?.(job);
|
|
117
|
+
});
|
|
118
|
+
return job;
|
|
119
|
+
}
|
|
120
|
+
export function pauseCatchUp() {
|
|
121
|
+
controller?.abort();
|
|
122
|
+
return job;
|
|
123
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ModelPort } from "../core/profile-analysis.js";
|
|
2
2
|
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
3
3
|
import { userPromptManager } from "../services/user-prompt/user-prompt-manager.js";
|
|
4
4
|
import type { MemoryHost } from "../types/index.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isStructuredSummaryPromptMessage } from "../core/internal-prompt.js";
|
|
2
|
-
import { analyzeProfile } from "../core/profile-analysis.js";
|
|
3
2
|
import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
|
|
4
3
|
import { getTags } from "../services/tags.js";
|
|
5
4
|
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
@@ -7,6 +6,7 @@ import { userPromptManager } from "../services/user-prompt/user-prompt-manager.j
|
|
|
7
6
|
import { buildImportKey } from "./importer.js";
|
|
8
7
|
import { existsSync } from "node:fs";
|
|
9
8
|
import { ImportLedger, importLedgerDbPath } from "./ledger.js";
|
|
9
|
+
import { drainProfileBacklog } from "./profile-backlog.js";
|
|
10
10
|
/** Record historical prompts and build or update the user profile in batches. */
|
|
11
11
|
export async function importProfileFromHistory(source, options) {
|
|
12
12
|
if (!options.dryRun && !options.model)
|
|
@@ -80,39 +80,18 @@ export async function importProfileFromHistory(source, options) {
|
|
|
80
80
|
report.error = "Profile import needs a user email";
|
|
81
81
|
return report;
|
|
82
82
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (existing) {
|
|
97
|
-
if (!analysis.merged ||
|
|
98
|
-
!(await profiles.updateProfile(existing.id, analysis.merged, batch.length, `History import of ${batch.length} prompts`)))
|
|
99
|
-
throw new Error("Profile update conflict");
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
await profiles.createProfile(user.userEmail, user.displayName || user.userEmail, user.userName || user.userEmail, user.userEmail, analysis.raw, batch.length);
|
|
103
|
-
}
|
|
104
|
-
await prompts.markMultipleAsUserLearningCaptured(batch.map((item) => item.id));
|
|
105
|
-
report.batchesBuilt++;
|
|
106
|
-
succeeded = true;
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
if (attempt === 1) {
|
|
110
|
-
report.error = error instanceof Error ? error.message : String(error);
|
|
111
|
-
report.remaining = await prompts.countUnanalyzedForUserLearning();
|
|
112
|
-
return report;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
83
|
+
const drained = await drainProfileBacklog({
|
|
84
|
+
user: { ...user, userEmail: user.userEmail },
|
|
85
|
+
model: options.model,
|
|
86
|
+
batchSize,
|
|
87
|
+
signal: options.signal,
|
|
88
|
+
label: "History import",
|
|
89
|
+
promptStore: prompts,
|
|
90
|
+
profileStore: profiles,
|
|
91
|
+
});
|
|
92
|
+
report.batchesBuilt += drained.batchesBuilt;
|
|
93
|
+
report.remaining = drained.remaining;
|
|
94
|
+
if (drained.error)
|
|
95
|
+
report.error = drained.error;
|
|
117
96
|
return report;
|
|
118
97
|
}
|
|
@@ -23,6 +23,31 @@ export function captureFailureHealth(byModel, byReason) {
|
|
|
23
23
|
: "No recent attempts",
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
/** Claude Code rows from the settings snapshot; missing data is a failure, never a throw. */
|
|
27
|
+
function claudeCodeRows(snapshot) {
|
|
28
|
+
const model = snapshot.effective?.["claude-code"];
|
|
29
|
+
const folder = snapshot.claudeFolder;
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
check: "Claude Code model",
|
|
33
|
+
status: model?.ready ? "pass" : "fail",
|
|
34
|
+
reason: !model
|
|
35
|
+
? "Claude Code model status is unavailable"
|
|
36
|
+
: model.ready
|
|
37
|
+
? "Resolved: external API"
|
|
38
|
+
: (model.issues ?? []).join("; ") || "External API is not configured",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
check: "Claude Code folder",
|
|
42
|
+
status: !folder ? "fail" : folder.exists ? "pass" : "warn",
|
|
43
|
+
reason: !folder
|
|
44
|
+
? "Claude Code folder status is unavailable"
|
|
45
|
+
: folder.exists
|
|
46
|
+
? `Found: ${folder.root}`
|
|
47
|
+
: `Folder not found: ${folder.root}`,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
26
51
|
/** Run checks independently so a failed dependency cannot hide the remaining checks. */
|
|
27
52
|
export async function runSettingsHealth(input) {
|
|
28
53
|
const checks = [];
|
|
@@ -42,9 +67,10 @@ export async function runSettingsHealth(input) {
|
|
|
42
67
|
});
|
|
43
68
|
}
|
|
44
69
|
}
|
|
70
|
+
let snapshot = {};
|
|
45
71
|
await check("Config files", async () => {
|
|
46
72
|
const { getSettingsSnapshot } = await import("../services/settings-snapshot.js");
|
|
47
|
-
getSettingsSnapshot(input.directory);
|
|
73
|
+
snapshot = (getSettingsSnapshot(input.directory) ?? {});
|
|
48
74
|
return "Global and project config parsed";
|
|
49
75
|
});
|
|
50
76
|
await check("Memory store", async () => {
|
|
@@ -81,6 +107,7 @@ export async function runSettingsHealth(input) {
|
|
|
81
107
|
status: pi.kind === "unready" ? "fail" : "pass",
|
|
82
108
|
reason: pi.kind === "unready" ? pi.issues.join("; ") : `Resolved: ${pi.kind}`,
|
|
83
109
|
});
|
|
110
|
+
checks.push(...claudeCodeRows(snapshot));
|
|
84
111
|
try {
|
|
85
112
|
const { queryCaptureAttempts } = await import("../services/capture-attempt-store.js");
|
|
86
113
|
const { byModel, byReason } = await queryCaptureAttempts(Date.now() - 86400000, Date.now());
|
|
@@ -94,6 +121,16 @@ export async function runSettingsHealth(input) {
|
|
|
94
121
|
});
|
|
95
122
|
}
|
|
96
123
|
if (input.testModels) {
|
|
124
|
+
// Pi, Claude Code, and OpenCode may all use the external API; one health run calls it once.
|
|
125
|
+
let externalProbe;
|
|
126
|
+
const probeExternal = () => (externalProbe ??= import("./model-selection.js").then(async ({ selectImportModel }) => {
|
|
127
|
+
await selectImportModel({}).capture.summarize({
|
|
128
|
+
userPrompt: PROBE,
|
|
129
|
+
context: PROBE,
|
|
130
|
+
sessionId: "health",
|
|
131
|
+
projectDirectory: input.directory,
|
|
132
|
+
});
|
|
133
|
+
}));
|
|
97
134
|
const { resolveOpencodeHostModel } = await import("../services/ai/live-model-choice.js");
|
|
98
135
|
const { getOpencodeHostModels } = await import("./backfill-controls.js");
|
|
99
136
|
const opencodeRef = resolveOpencodeHostModel(CONFIG);
|
|
@@ -107,9 +144,11 @@ export async function runSettingsHealth(input) {
|
|
|
107
144
|
}
|
|
108
145
|
else {
|
|
109
146
|
await check("OpenCode model test", async () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
if (!opencodeRef) {
|
|
148
|
+
await probeExternal();
|
|
149
|
+
return "Fixed prompt completed";
|
|
150
|
+
}
|
|
151
|
+
const { capture } = await createOpencodeImportModels(opencodeRef, input.directory);
|
|
113
152
|
await capture.summarize({
|
|
114
153
|
userPrompt: PROBE,
|
|
115
154
|
context: PROBE,
|
|
@@ -121,13 +160,7 @@ export async function runSettingsHealth(input) {
|
|
|
121
160
|
}
|
|
122
161
|
if (pi.kind === "manual") {
|
|
123
162
|
await check("Pi model test", async () => {
|
|
124
|
-
|
|
125
|
-
await selectImportModel({}).capture.summarize({
|
|
126
|
-
userPrompt: PROBE,
|
|
127
|
-
context: PROBE,
|
|
128
|
-
sessionId: "health",
|
|
129
|
-
projectDirectory: input.directory,
|
|
130
|
-
});
|
|
163
|
+
await probeExternal();
|
|
131
164
|
return "Fixed prompt completed";
|
|
132
165
|
});
|
|
133
166
|
}
|
|
@@ -138,6 +171,12 @@ export async function runSettingsHealth(input) {
|
|
|
138
171
|
reason: "Pi models need an active Pi session to test",
|
|
139
172
|
});
|
|
140
173
|
}
|
|
174
|
+
if (snapshot.effective?.["claude-code"]?.ready) {
|
|
175
|
+
await check("Claude Code model test", async () => {
|
|
176
|
+
await probeExternal();
|
|
177
|
+
return "Fixed prompt completed";
|
|
178
|
+
});
|
|
179
|
+
}
|
|
141
180
|
}
|
|
142
181
|
return { checks };
|
|
143
182
|
}
|
|
@@ -10,3 +10,4 @@ export { stopStandaloneOpencodeReads } from "./opencode-standalone-models.js";
|
|
|
10
10
|
export { BackfillControls } from "./backfill-controls.js";
|
|
11
11
|
export { directoryMapsView } from "./map-suggestions.js";
|
|
12
12
|
export { testExternalApi } from "./external-api-test.js";
|
|
13
|
+
export { catchUpState, pauseCatchUp, previewCatchUp, startCatchUp } from "./profile-catch-up.js";
|
|
@@ -10,3 +10,4 @@ export { stopStandaloneOpencodeReads } from "./opencode-standalone-models.js";
|
|
|
10
10
|
export { BackfillControls } from "./backfill-controls.js";
|
|
11
11
|
export { directoryMapsView } from "./map-suggestions.js";
|
|
12
12
|
export { testExternalApi } from "./external-api-test.js";
|
|
13
|
+
export { catchUpState, pauseCatchUp, previewCatchUp, startCatchUp } from "./profile-catch-up.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { performAutoCapture } from "./services/auto-capture.js";
|
|
|
5
5
|
import { createOpenCodeAutoCaptureHost } from "./adapters/opencode/auto-capture-host.js";
|
|
6
6
|
import { isInternalPrompt, isStructuredSummaryPromptMessage, recordUserPrompt, } from "./adapters/opencode/user-prompt.js";
|
|
7
7
|
import { executeMemoryOperation } from "./core/memory-operations.js";
|
|
8
|
+
import { memoryToolDescription } from "./core/memory-tool-text.js";
|
|
8
9
|
import { OPENCODE_IMPORT_COMMAND, OPENCODE_IMPORT_DESCRIPTION, latestUserMessageModel, runOpencodeImportCommand, } from "./adapters/opencode/import-command.js";
|
|
9
10
|
import { buildRecentMemoriesSection, formatMemoriesForCompaction } from "./core/retrieval.js";
|
|
10
11
|
import { performUserProfileLearning } from "./adapters/opencode/profile-learning.js";
|
|
@@ -17,6 +18,7 @@ import { resolveOpencodeHostModel } from "./services/ai/live-model-choice.js";
|
|
|
17
18
|
import { log } from "./services/logger.js";
|
|
18
19
|
import { getLanguageName } from "./services/language-detector.js";
|
|
19
20
|
import { getHostClientConfig } from "./adapters/opencode/opencode-host-config.js";
|
|
21
|
+
import { applySkillsPathConfig } from "./adapters/opencode/package-skills.js";
|
|
20
22
|
import { loadOpencodeProvider } from "./adapters/opencode/opencode-provider-loader.js";
|
|
21
23
|
import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./adapters/opencode/opencode-provider.js";
|
|
22
24
|
import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./adapters/opencode/internal-capture-sessions.js";
|
|
@@ -372,6 +374,7 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
372
374
|
config: async (cfg) => {
|
|
373
375
|
applyStructuredOutputAgentConfig(cfg);
|
|
374
376
|
applyHistoryImportCommandConfig(cfg);
|
|
377
|
+
applySkillsPathConfig(cfg);
|
|
375
378
|
},
|
|
376
379
|
// V1 runs slash commands as a model turn; do the import here and hand the
|
|
377
380
|
// turn only the finished report to relay.
|
|
@@ -472,7 +475,7 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
472
475
|
},
|
|
473
476
|
tool: {
|
|
474
477
|
memory: tool({
|
|
475
|
-
description:
|
|
478
|
+
description: memoryToolDescription(getLanguageName(CONFIG.autoCaptureLanguage || "en")),
|
|
476
479
|
args: {
|
|
477
480
|
mode: tool.schema
|
|
478
481
|
.enum([
|