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.
Files changed (64) hide show
  1. package/dist/adapters/opencode/package-skills.d.ts +16 -0
  2. package/dist/adapters/opencode/package-skills.js +60 -0
  3. package/dist/adapters/opencode/profile-learning.js +9 -2
  4. package/dist/adapters/pi/extension.js +5 -1
  5. package/dist/adapters/pi/profile.js +7 -1
  6. package/dist/cli/index.js +5 -0
  7. package/dist/cli/profile-catch-up-command.d.ts +2 -0
  8. package/dist/cli/profile-catch-up-command.js +110 -0
  9. package/dist/cli/web-command.js +3 -6
  10. package/dist/config.d.ts +1 -1
  11. package/dist/config.js +6 -4
  12. package/dist/core/memory-tool-text.d.ts +4 -0
  13. package/dist/core/memory-tool-text.js +6 -0
  14. package/dist/core/profile-backoff.d.ts +20 -0
  15. package/dist/core/profile-backoff.js +41 -0
  16. package/dist/core/profile-failure.d.ts +16 -0
  17. package/dist/core/profile-failure.js +34 -0
  18. package/dist/core/trivial-prompt.d.ts +5 -0
  19. package/dist/core/trivial-prompt.js +8 -0
  20. package/dist/importer/claude-hook-api.js +13 -2
  21. package/dist/importer/model-selection.d.ts +2 -0
  22. package/dist/importer/model-selection.js +15 -4
  23. package/dist/importer/profile-backlog.d.ts +42 -0
  24. package/dist/importer/profile-backlog.js +79 -0
  25. package/dist/importer/profile-catch-up.d.ts +39 -0
  26. package/dist/importer/profile-catch-up.js +123 -0
  27. package/dist/importer/profile-import.d.ts +1 -1
  28. package/dist/importer/profile-import.js +14 -35
  29. package/dist/importer/settings-health.js +50 -11
  30. package/dist/importer/web-import-api.d.ts +1 -0
  31. package/dist/importer/web-import-api.js +1 -0
  32. package/dist/index.js +4 -1
  33. package/dist/services/api-tokens.d.ts +35 -0
  34. package/dist/services/api-tokens.js +185 -0
  35. package/dist/services/capture-attempt-store.d.ts +3 -1
  36. package/dist/services/capture-attempt-store.js +12 -5
  37. package/dist/services/context.js +4 -1
  38. package/dist/services/embedding-change.d.ts +52 -0
  39. package/dist/services/embedding-change.js +177 -0
  40. package/dist/services/embedding.d.ts +23 -1
  41. package/dist/services/embedding.js +63 -24
  42. package/dist/services/global-config-writer.d.ts +12 -1
  43. package/dist/services/global-config-writer.js +32 -4
  44. package/dist/services/migration-service.js +4 -1
  45. package/dist/services/settings-snapshot.d.ts +8 -0
  46. package/dist/services/settings-snapshot.js +11 -0
  47. package/dist/services/user-prompt/profile-catch-up-lease.d.ts +34 -0
  48. package/dist/services/user-prompt/profile-catch-up-lease.js +77 -0
  49. package/dist/services/user-prompt/user-prompt-manager.d.ts +14 -1
  50. package/dist/services/user-prompt/user-prompt-manager.js +36 -1
  51. package/dist/services/web-api-auth.d.ts +6 -2
  52. package/dist/services/web-api-auth.js +11 -9
  53. package/dist/services/web-password.d.ts +11 -0
  54. package/dist/services/web-password.js +30 -0
  55. package/dist/services/web-server.d.ts +8 -0
  56. package/dist/services/web-server.js +182 -19
  57. package/dist/v2/adapter.js +3 -0
  58. package/dist/web/assets/index-B6LyHM22.css +2 -0
  59. package/dist/web/assets/index-CVvXO3zL.js +125 -0
  60. package/dist/web/index.html +2 -2
  61. package/package.json +5 -1
  62. package/skills/omms-memory/SKILL.md +93 -0
  63. package/dist/web/assets/index-Bh5aFNPX.js +0 -124
  64. 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
- log("Claude Code profile learning failed", { code: errorCode(error) });
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 provider = AIProviderFactory.createProvider(providerName, buildMemoryProviderConfig({
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 provider.executeToolCall(system, prompt, tool, `history-profile-${randomUUID()}`);
98
+ const result = await profileProvider.executeToolCall(system, prompt, tool, `history-profile-${randomUUID()}`);
88
99
  if (!result.success || !result.data)
89
- throw new Error("History profile model call failed");
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 { type ModelPort } from "../core/profile-analysis.js";
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 model = options.model;
84
- while ((report.remaining = await prompts.countUnanalyzedForUserLearning()) > 0) {
85
- if (options.signal?.aborted)
86
- break;
87
- const batch = await prompts.getPromptsForUserLearning(batchSize);
88
- if (batch.length === 0)
89
- break;
90
- let succeeded = false;
91
- for (let attempt = 0; attempt < 2 && !succeeded; attempt++) {
92
- try {
93
- const existing = await profiles.getActiveProfile(user.userEmail);
94
- const context = batch.map((item, index) => `${index + 1}. ${item.content}`).join("\n");
95
- const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
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
- const capture = opencodeRef
111
- ? (await createOpencodeImportModels(opencodeRef, input.directory)).capture
112
- : (await import("./model-selection.js")).selectImportModel({}).capture;
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
- const { selectImportModel } = await import("./model-selection.js");
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: `Manage and query project memory (MATCH USER LANGUAGE: ${getLanguageName(CONFIG.autoCaptureLanguage || "en")}). Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`,
478
+ description: memoryToolDescription(getLanguageName(CONFIG.autoCaptureLanguage || "en")),
476
479
  args: {
477
480
  mode: tool.schema
478
481
  .enum([