om-memory-system 5.9.0 → 5.10.1

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 (93) hide show
  1. package/README.md +7 -11
  2. package/bin/omms-launch.mjs +8 -4
  3. package/dist/adapters/opencode/conversation.js +4 -2
  4. package/dist/adapters/opencode/memory-tool.d.ts +4 -0
  5. package/dist/adapters/opencode/memory-tool.js +53 -0
  6. package/dist/adapters/opencode/package-skills.d.ts +1 -1
  7. package/dist/adapters/opencode/package-skills.js +17 -15
  8. package/dist/adapters/pi/extension.js +7 -0
  9. package/dist/cli/memory-command.d.ts +2 -0
  10. package/dist/cli/memory-command.js +35 -9
  11. package/dist/cli/web-command.js +25 -9
  12. package/dist/cli/web-step-aside.d.ts +17 -0
  13. package/dist/cli/web-step-aside.js +34 -0
  14. package/dist/config-paths.d.ts +5 -0
  15. package/dist/config-paths.js +15 -0
  16. package/dist/config-template.d.ts +1 -1
  17. package/dist/config-template.js +8 -0
  18. package/dist/config.d.ts +51 -40
  19. package/dist/config.js +8 -15
  20. package/dist/core/capture.d.ts +2 -1
  21. package/dist/core/capture.js +8 -1
  22. package/dist/core/host.d.ts +12 -0
  23. package/dist/core/memorisation-operations.d.ts +33 -0
  24. package/dist/core/memorisation-operations.js +160 -0
  25. package/dist/core/memorisation-settings.d.ts +11 -0
  26. package/dist/core/memorisation-settings.js +48 -0
  27. package/dist/core/memorisation.d.ts +43 -0
  28. package/dist/core/memorisation.js +76 -0
  29. package/dist/core/memorise-turn.d.ts +11 -0
  30. package/dist/core/memorise-turn.js +64 -0
  31. package/dist/core/memory-operations.d.ts +6 -1
  32. package/dist/core/memory-operations.js +29 -0
  33. package/dist/core/memory-tool-text.js +1 -1
  34. package/dist/core/retrieval.d.ts +3 -2
  35. package/dist/core/retrieval.js +11 -4
  36. package/dist/core/tool-outcome.d.ts +6 -0
  37. package/dist/core/tool-outcome.js +31 -0
  38. package/dist/importer/claude-conversation.js +8 -1
  39. package/dist/importer/claude-turn-units.js +1 -0
  40. package/dist/importer/importer.js +3 -3
  41. package/dist/importer/opencode-reader.js +2 -1
  42. package/dist/importer/pi-conversation.js +11 -5
  43. package/dist/index.js +2 -42
  44. package/dist/services/api-handlers.js +13 -35
  45. package/dist/services/capture-diagnostics.d.ts +3 -2
  46. package/dist/services/capture-diagnostics.js +5 -1
  47. package/dist/services/cleanup-service.js +2 -1
  48. package/dist/services/client-session-read.d.ts +16 -0
  49. package/dist/services/client-session-read.js +32 -0
  50. package/dist/services/client.d.ts +6 -1
  51. package/dist/services/client.js +20 -36
  52. package/dist/services/deduplication-service.js +12 -1
  53. package/dist/services/global-config-writer.js +14 -1
  54. package/dist/services/linked-search.d.ts +15 -0
  55. package/dist/services/linked-search.js +23 -2
  56. package/dist/services/live-captured-entries.js +2 -1
  57. package/dist/services/memorisation-routes.d.ts +16 -0
  58. package/dist/services/memorisation-routes.js +94 -0
  59. package/dist/services/memorisation-store.d.ts +45 -0
  60. package/dist/services/memorisation-store.js +120 -0
  61. package/dist/services/memory-kind.d.ts +6 -0
  62. package/dist/services/memory-kind.js +6 -0
  63. package/dist/services/memory-portability-service.js +2 -1
  64. package/dist/services/memory-replace.d.ts +8 -0
  65. package/dist/services/memory-replace.js +26 -0
  66. package/dist/services/retrieval-rerank.d.ts +3 -1
  67. package/dist/services/retrieval-rerank.js +1 -1
  68. package/dist/services/settings-snapshot.js +6 -0
  69. package/dist/services/turso/exact-scan.d.ts +2 -1
  70. package/dist/services/turso/exact-scan.js +4 -3
  71. package/dist/services/turso/keyword-search.d.ts +2 -1
  72. package/dist/services/turso/keyword-search.js +3 -2
  73. package/dist/services/turso/vector-search.d.ts +10 -9
  74. package/dist/services/turso/vector-search.js +38 -95
  75. package/dist/services/turso/vector-writes.d.ts +8 -0
  76. package/dist/services/turso/vector-writes.js +80 -0
  77. package/dist/services/web-ensure.d.ts +1 -0
  78. package/dist/services/web-ensure.js +5 -1
  79. package/dist/services/web-memory-rows.d.ts +12 -0
  80. package/dist/services/web-memory-rows.js +58 -0
  81. package/dist/services/web-route-modules.js +5 -0
  82. package/dist/services/web-update.d.ts +6 -1
  83. package/dist/services/web-update.js +12 -1
  84. package/dist/v2/adapter.d.ts +13 -1
  85. package/dist/v2/adapter.js +7 -0
  86. package/dist/web/assets/index-CoYuPYCS.css +2 -0
  87. package/dist/web/assets/index-DQgI670D.js +171 -0
  88. package/dist/web/index.html +2 -2
  89. package/package.json +1 -1
  90. package/skills/omms-memory/SKILL.md +18 -0
  91. package/skills/omms-remember/SKILL.md +118 -0
  92. package/dist/web/assets/index-2BNRLGqe.css +0 -2
  93. package/dist/web/assets/index-Bhk1P5Z7.js +0 -170
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
2
2
  import { type KeywordMode } from "./utils/keyword-boost.js";
3
3
  import { type ActiveSpace } from "./services/spaces/active-space.js";
4
+ import * as memorisation from "./core/memorisation-settings.js";
4
5
  interface OmmsConfig {
5
6
  storagePath?: string;
6
7
  userEmailOverride?: string;
@@ -28,6 +29,10 @@ interface OmmsConfig {
28
29
  keywordMode?: KeywordMode;
29
30
  /** Shared budget for automatic memory context in approximate tokens (ceil(UTF-8 bytes / 4)). */
30
31
  retrievalMaxTokens?: number;
32
+ /** Memorisation size and repeat behaviour. Global config only. */
33
+ memorisationMaxSize?: number;
34
+ memorisationSizeUnit?: memorisation.MemorisationSettings["memorisationSizeUnit"];
35
+ memorisationRepeat?: memorisation.MemorisationSettings["memorisationRepeat"];
31
36
  maxProfileItems?: number;
32
37
  injectProfile?: boolean;
33
38
  containerTagPrefix?: string;
@@ -196,26 +201,6 @@ declare function buildConfig(fileConfig: OmmsConfig): {
196
201
  skillLearning: boolean;
197
202
  skillLearningRedact: boolean;
198
203
  skillFolders: string[] | undefined;
199
- storagePath: string;
200
- /** The global `storagePath` when a project moved `storagePath`; `skills.db` stays there. */
201
- skillStoragePath: string | undefined;
202
- userEmailOverride: string | undefined;
203
- userNameOverride: string | undefined;
204
- embeddingModel: string;
205
- embeddingDimensions: number;
206
- embeddingUseTaskPrefixes: boolean;
207
- embeddingApiUrl: string | undefined;
208
- embeddingApiKey: string | undefined;
209
- similarityThreshold: number;
210
- rerankerEnabled: boolean;
211
- rerankerApiUrl: string | undefined;
212
- rerankerModel: string | undefined;
213
- rerankerApiKey: string | undefined;
214
- rerankerTimeoutMs: number;
215
- keywordBoost: number;
216
- keywordMode: KeywordMode;
217
- maxMemories: number;
218
- retrievalMaxTokens: number;
219
204
  maxProfileItems: number;
220
205
  injectProfile: boolean;
221
206
  containerTagPrefix: string;
@@ -241,6 +226,29 @@ declare function buildConfig(fileConfig: OmmsConfig): {
241
226
  captureTraceRetentionDays: number;
242
227
  captureAttemptRetentionDays: number;
243
228
  captureRetryRetentionHours: number;
229
+ memorisationMaxSize: number;
230
+ memorisationSizeUnit: import("./core/memorisation.js").MemorisationSizeUnit;
231
+ memorisationRepeat: import("./core/memorisation.js").MemorisationRepeat;
232
+ storagePath: string;
233
+ /** The global `storagePath` when a project moved `storagePath`; `skills.db` stays there. */
234
+ skillStoragePath: string | undefined;
235
+ userEmailOverride: string | undefined;
236
+ userNameOverride: string | undefined;
237
+ embeddingModel: string;
238
+ embeddingDimensions: number;
239
+ embeddingUseTaskPrefixes: boolean;
240
+ embeddingApiUrl: string | undefined;
241
+ embeddingApiKey: string | undefined;
242
+ similarityThreshold: number;
243
+ rerankerEnabled: boolean;
244
+ rerankerApiUrl: string | undefined;
245
+ rerankerModel: string | undefined;
246
+ rerankerApiKey: string | undefined;
247
+ rerankerTimeoutMs: number;
248
+ keywordBoost: number;
249
+ keywordMode: KeywordMode;
250
+ maxMemories: number;
251
+ retrievalMaxTokens: number;
244
252
  };
245
253
  export declare function getGlobalConfigSourcePath(): string | undefined;
246
254
  export declare function getGlobalConfigWritePath(): string;
@@ -310,26 +318,6 @@ export declare let CONFIG: {
310
318
  skillLearning: boolean;
311
319
  skillLearningRedact: boolean;
312
320
  skillFolders: string[] | undefined;
313
- storagePath: string;
314
- /** The global `storagePath` when a project moved `storagePath`; `skills.db` stays there. */
315
- skillStoragePath: string | undefined;
316
- userEmailOverride: string | undefined;
317
- userNameOverride: string | undefined;
318
- embeddingModel: string;
319
- embeddingDimensions: number;
320
- embeddingUseTaskPrefixes: boolean;
321
- embeddingApiUrl: string | undefined;
322
- embeddingApiKey: string | undefined;
323
- similarityThreshold: number;
324
- rerankerEnabled: boolean;
325
- rerankerApiUrl: string | undefined;
326
- rerankerModel: string | undefined;
327
- rerankerApiKey: string | undefined;
328
- rerankerTimeoutMs: number;
329
- keywordBoost: number;
330
- keywordMode: KeywordMode;
331
- maxMemories: number;
332
- retrievalMaxTokens: number;
333
321
  maxProfileItems: number;
334
322
  injectProfile: boolean;
335
323
  containerTagPrefix: string;
@@ -355,6 +343,29 @@ export declare let CONFIG: {
355
343
  captureTraceRetentionDays: number;
356
344
  captureAttemptRetentionDays: number;
357
345
  captureRetryRetentionHours: number;
346
+ memorisationMaxSize: number;
347
+ memorisationSizeUnit: import("./core/memorisation.js").MemorisationSizeUnit;
348
+ memorisationRepeat: import("./core/memorisation.js").MemorisationRepeat;
349
+ storagePath: string;
350
+ /** The global `storagePath` when a project moved `storagePath`; `skills.db` stays there. */
351
+ skillStoragePath: string | undefined;
352
+ userEmailOverride: string | undefined;
353
+ userNameOverride: string | undefined;
354
+ embeddingModel: string;
355
+ embeddingDimensions: number;
356
+ embeddingUseTaskPrefixes: boolean;
357
+ embeddingApiUrl: string | undefined;
358
+ embeddingApiKey: string | undefined;
359
+ similarityThreshold: number;
360
+ rerankerEnabled: boolean;
361
+ rerankerApiUrl: string | undefined;
362
+ rerankerModel: string | undefined;
363
+ rerankerApiKey: string | undefined;
364
+ rerankerTimeoutMs: number;
365
+ keywordBoost: number;
366
+ keywordMode: KeywordMode;
367
+ maxMemories: number;
368
+ retrievalMaxTokens: number;
358
369
  };
359
370
  type RuntimeConfig = ReturnType<typeof buildConfig>;
360
371
  export { getAutoCaptureProviderStatus, type AutoCaptureProviderStatus, } from "./services/ai/live-model-choice.js";
package/dist/config.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { homedir } from "node:os";
4
3
  import { stripJsoncComments } from "./services/jsonc.js";
5
4
  import { isValidClaudeConfigDir } from "./services/claude-folder.js";
6
5
  import { log } from "./services/logger.js";
@@ -22,20 +21,8 @@ import { expandPath, storagePathFrom, withSkillStorage, } from "./services/space
22
21
  import { resolveActiveSpace } from "./services/spaces/active-space.js";
23
22
  import { backfillSpaceLabelsFromDisk } from "./services/spaces/space-label.js";
24
23
  import { resetStoreHandles } from "./services/spaces/store-handles.js";
25
- const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
26
- const DATA_DIR = join(homedir(), ".omms");
27
- const LEGACY_CONFIG_FILES = [
28
- join(homedir(), ".config", "opencode", "opencode-mem.jsonc"),
29
- join(homedir(), ".config", "opencode", "opencode-mem.json"),
30
- ];
31
- // Dual-read: the omms config wins; the legacy opencode-mem config is read
32
- // only when no omms config file exists (decision D13 rule 6). The legacy
33
- // file is never written.
34
- const CONFIG_FILES = [
35
- join(OMMS_CONFIG_DIR, "omms.jsonc"),
36
- join(OMMS_CONFIG_DIR, "omms.json"),
37
- ...LEGACY_CONFIG_FILES,
38
- ];
24
+ import * as memorisation from "./core/memorisation-settings.js";
25
+ import { CONFIG_FILES, DATA_DIR, LEGACY_CONFIG_FILES, OMMS_CONFIG_DIR } from "./config-paths.js";
39
26
  if (!existsSync(OMMS_CONFIG_DIR))
40
27
  mkdirSync(OMMS_CONFIG_DIR, { recursive: true });
41
28
  if (!existsSync(DATA_DIR))
@@ -51,6 +38,9 @@ const DEFAULTS = {
51
38
  rerankerEnabled: false,
52
39
  rerankerTimeoutMs: RERANKER_TIMEOUT_DEFAULT_MS,
53
40
  retrievalMaxTokens: 2000,
41
+ memorisationMaxSize: 2000,
42
+ memorisationSizeUnit: "characters",
43
+ memorisationRepeat: "add",
54
44
  maxProfileItems: 5,
55
45
  injectProfile: true,
56
46
  // omms since the tag prefix migration: new memories carry the fork's
@@ -325,6 +315,7 @@ function buildConfig(fileConfig) {
325
315
  keywordMode: checkedKeywordMode(fileConfig.keywordMode),
326
316
  maxMemories,
327
317
  retrievalMaxTokens,
318
+ ...memorisation.settingsFrom(fileConfig),
328
319
  maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems,
329
320
  injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile,
330
321
  containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix,
@@ -526,6 +517,8 @@ export function initConfig(directory, options = {}) {
526
517
  delete projectOverrides.captureTraceRetentionDays;
527
518
  // The queue holds conversation text, so only the global file sets how long.
528
519
  delete projectOverrides.captureRetryRetentionHours;
520
+ for (const key of memorisation.MEMORISATION_KEYS)
521
+ delete projectOverrides[key];
529
522
  // Skill learning stores full turns: a project may only turn it off.
530
523
  const skillRule = projectSkillOverride(projectConfig);
531
524
  if (skillRule.ignoredTurnOn) {
@@ -1,5 +1,5 @@
1
1
  import { type CaptureFailureInfo } from "./capture-retry-policy.js";
2
- import type { CaptureConversation, CapturePromptContext, CaptureProvenance, CaptureSummaryProvider } from "./host.js";
2
+ import type { CaptureConversation, CapturePromptContext, CaptureProvenance, CaptureSkipReason, CaptureSummaryProvider } from "./host.js";
3
3
  export interface CaptureWorkUnit extends CaptureProvenance, CaptureConversation {
4
4
  projectDirectory: string;
5
5
  userPrompt: string;
@@ -12,6 +12,7 @@ export type CaptureResult = {
12
12
  } | {
13
13
  status: "skipped";
14
14
  type?: string;
15
+ reason?: CaptureSkipReason;
15
16
  };
16
17
  /**
17
18
  * Thrown by `captureConversation` for a failed attempt. Keeps the original
@@ -3,6 +3,7 @@ import { buildCaptureAttemptRecord, emitCaptureAttempt } from "../services/captu
3
3
  import { memoryClient } from "../services/client.js";
4
4
  import { getTags } from "../services/tags.js";
5
5
  import { buildMarkdownContext, getAutoCaptureMarkdownBudget } from "./capture-context.js";
6
+ import { turnSavedMemorisation } from "./memorise-turn.js";
6
7
  import { errorHttpStatus, errorRetryAfterMs, } from "./capture-retry-policy.js";
7
8
  /**
8
9
  * Thrown by `captureConversation` for a failed attempt. Keeps the original
@@ -18,7 +19,7 @@ export class CaptureAttemptError extends Error {
18
19
  }
19
20
  async function getLatestProjectMemory(containerTag) {
20
21
  try {
21
- const result = await memoryClient.listMemories(containerTag, 1);
22
+ const result = await memoryClient.listMemories(containerTag, 1, "project", { kind: "normal" });
22
23
  if (!result.success || result.memories.length === 0)
23
24
  return null;
24
25
  const latest = result.memories[0];
@@ -65,6 +66,12 @@ export async function captureConversation(workUnit, provider) {
65
66
  }
66
67
  }
67
68
  async function runCapture(workUnit, provider, diagnostics, markdownBudget) {
69
+ // A memorisation reaches an agent only through `recall`. Capturing the turn
70
+ // that saved one would put the snapshot back into per-prompt recall.
71
+ if (turnSavedMemorisation(workUnit.toolCalls)) {
72
+ diagnostics.skipReason = "memorisation-turn";
73
+ return { status: "skipped", reason: "memorisation-turn" };
74
+ }
68
75
  const tags = getTags(workUnit.projectDirectory);
69
76
  const latestMemory = await getLatestProjectMemory(tags.project.tag);
70
77
  const context = buildMarkdownContext(workUnit.userPrompt, workUnit.textResponses, workUnit.toolCalls, latestMemory, markdownBudget);
@@ -1,5 +1,11 @@
1
1
  import type { MemoryHost, MemorySourceType } from "../types/index.js";
2
2
  export type CaptureToolOutcome = "ok" | "error" | "unknown";
3
+ /**
4
+ * Set only on a `memory` tool call in `memorise` mode: `saved` once its result
5
+ * reported success, `failed` when it did not, `unknown` until a result answers.
6
+ * Automatic capture skips a turn that holds a `saved` call.
7
+ */
8
+ export type CaptureMemoriseState = "saved" | "failed" | "unknown";
3
9
  export interface CaptureToolCall {
4
10
  name: string;
5
11
  input: string;
@@ -9,6 +15,8 @@ export interface CaptureToolCall {
9
15
  error?: string;
10
16
  /** Full path of a read-type call, for skill detection. Memory capture ignores it. */
11
17
  path?: string;
18
+ /** Set by the host readers on a memorise call. Capture skips the turn when it is `saved`. */
19
+ memorise?: CaptureMemoriseState;
12
20
  }
13
21
  export interface CaptureConversation {
14
22
  textResponses: string[];
@@ -30,6 +38,8 @@ export type CaptureAttemptOutcome = "saved" | "skipped" | "failed";
30
38
  /** Fixed failure codes, in the order classification checks them. */
31
39
  export declare const CAPTURE_FAILURE_REASONS: readonly ["call-error", "empty-text", "truncated", "invalid-json", "schema-mismatch", "persist-error"];
32
40
  export type CaptureFailureReason = (typeof CAPTURE_FAILURE_REASONS)[number];
41
+ /** Why capture ended a turn as skipped before the capture model ran. */
42
+ export type CaptureSkipReason = "memorisation-turn";
33
43
  export type CaptureExtractionPath = "host-model" | "external-api";
34
44
  /**
35
45
  * Filled in by the extraction path during one capture attempt. The capture
@@ -47,6 +57,8 @@ export interface CaptureAttemptDiagnostics {
47
57
  userPrompt?: string;
48
58
  rawReply?: string;
49
59
  failureReason?: CaptureFailureReason;
60
+ /** Set when capture skipped the turn without calling the capture model. */
61
+ skipReason?: CaptureSkipReason;
50
62
  /** HTTP status of a failed model call, when the path knows it. */
51
63
  httpStatus?: number;
52
64
  /** The wait a failed model call asked for in its `Retry-After` header. */
@@ -0,0 +1,33 @@
1
+ import { type MemoryScope } from "../services/client.js";
2
+ import type { LinkedSearchSettings } from "../services/linked-search.js";
3
+ import type { getTags } from "../services/tags.js";
4
+ import type { MemoryHost } from "../types/index.js";
5
+ import { type MemorisationSettings, type MemorisationSnapshot } from "./memorisation.js";
6
+ export interface MemoriseArgs extends MemorisationSnapshot {
7
+ content?: string;
8
+ tags?: string;
9
+ }
10
+ export interface RecallArgs {
11
+ /** A memorisation ID (`ms-4k7q2p`) or free text. */
12
+ query?: string;
13
+ /** Also read as an ID or text when `query` is empty. */
14
+ memoryId?: string;
15
+ content?: string;
16
+ /** Comma-separated labels. Only memorisations that carry all of them match. */
17
+ tags?: string;
18
+ limit?: number;
19
+ scope?: MemoryScope;
20
+ }
21
+ interface MemorisationContext {
22
+ host?: MemoryHost;
23
+ hostSessionId?: string;
24
+ }
25
+ /** Saves an exact memorisation: explicit text or a session snapshot. */
26
+ export declare function runMemorise(args: MemoriseArgs, context: MemorisationContext, tags: ReturnType<typeof getTags>, settings: MemorisationSettings, linked: LinkedSearchSettings): Promise<Record<string, unknown>>;
27
+ /**
28
+ * Fetches memorisations by exact ID, or by text across memorisations only. `tags` keeps only
29
+ * memorisations that carry every label. With `tags` and no text it returns the newest ones.
30
+ * An exact ID ignores `tags`.
31
+ */
32
+ export declare function runRecall(args: RecallArgs, tags: ReturnType<typeof getTags>, scope: MemoryScope | undefined, linked: LinkedSearchSettings): Promise<Record<string, unknown>>;
33
+ export {};
@@ -0,0 +1,160 @@
1
+ import { memoryClient } from "../services/client.js";
2
+ import { findSnapshotByHostSession, getMemorisationById, listMemorisationsByTags, memorisationIdTaken, } from "../services/memorisation-store.js";
3
+ import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
4
+ import { hasAllTags, isMemorisationId, MEMORISATION_TAG, memorisationId, memorisationSizeError, parseTagList, pickRecallMatches, snapshotText, visibleTags, } from "./memorisation.js";
5
+ const SNAPSHOT_FIELDS = ["topic", "done", "doing", "next"];
6
+ const BLOCKED = { success: false, error: "Private content blocked" };
7
+ /** Strips `<private>` text. A field that is fully private is dropped. */
8
+ function cleanField(value) {
9
+ if (!value?.trim() || isFullyPrivate(value))
10
+ return undefined;
11
+ return stripPrivateContent(value);
12
+ }
13
+ function callerTags(tags) {
14
+ return [...new Set([MEMORISATION_TAG, ...parseTagList(tags)])];
15
+ }
16
+ /** Builds the text to store, or the refusal to return. */
17
+ function memorisationText(args) {
18
+ const hasContent = Boolean(args.content?.trim());
19
+ const hasSnapshot = SNAPSHOT_FIELDS.some((field) => args[field]?.trim());
20
+ if (hasContent && hasSnapshot) {
21
+ return {
22
+ success: false,
23
+ error: "Give content or snapshot fields (topic, done, doing, next), not both",
24
+ };
25
+ }
26
+ if (!hasContent && !hasSnapshot) {
27
+ return {
28
+ success: false,
29
+ error: "content or at least one snapshot field (topic, done, doing, next) required",
30
+ };
31
+ }
32
+ if (hasContent) {
33
+ if (isFullyPrivate(args.content))
34
+ return BLOCKED;
35
+ return { kind: "explicit", text: stripPrivateContent(args.content) };
36
+ }
37
+ const text = snapshotText({
38
+ topic: cleanField(args.topic),
39
+ done: cleanField(args.done),
40
+ doing: cleanField(args.doing),
41
+ next: cleanField(args.next),
42
+ });
43
+ return text ? { kind: "snapshot", text } : BLOCKED;
44
+ }
45
+ /** Saves an exact memorisation: explicit text or a session snapshot. */
46
+ export async function runMemorise(args, context, tags, settings, linked) {
47
+ const built = memorisationText(args);
48
+ if ("success" in built)
49
+ return built;
50
+ const sizeError = memorisationSizeError(built.text, settings);
51
+ if (sizeError)
52
+ return { success: false, error: sizeError };
53
+ const sessionId = context.hostSessionId;
54
+ const wantsReplace = built.kind === "snapshot" && settings.memorisationRepeat === "replace";
55
+ const earlier = wantsReplace && sessionId ? await findSnapshotByHostSession(sessionId, tags.project.tag) : null;
56
+ const id = earlier?.id ?? (await memorisationId((candidate) => memorisationIdTaken(candidate, linked)));
57
+ const recordTags = callerTags(args.tags);
58
+ const info = tags.project;
59
+ const result = await memoryClient.addMemory(built.text, info.tag, {
60
+ id,
61
+ // The client swaps the earlier row for this one in one transaction, so a failed write keeps it.
62
+ replaceExisting: Boolean(earlier),
63
+ type: "memorisation",
64
+ tags: recordTags,
65
+ memorisation: built.kind,
66
+ host: context.host,
67
+ hostSessionId: sessionId,
68
+ source: "manual",
69
+ displayName: info.displayName,
70
+ userName: info.userName,
71
+ userEmail: info.userEmail,
72
+ projectPath: info.projectPath,
73
+ projectName: info.projectName,
74
+ gitRepoUrl: info.gitRepoUrl,
75
+ });
76
+ if (!result.success)
77
+ return { success: false, error: result.error };
78
+ const note = wantsReplace && !sessionId
79
+ ? " It could not replace an earlier snapshot because the host gave no session ID, so it was added."
80
+ : "";
81
+ return {
82
+ success: true,
83
+ message: `Memorisation saved as ${id}.${note}`,
84
+ id,
85
+ replaced: Boolean(earlier),
86
+ tags: recordTags,
87
+ };
88
+ }
89
+ function toIso(value) {
90
+ return typeof value === "number" ? new Date(value).toISOString() : undefined;
91
+ }
92
+ /** How many labelled memorisations a recall with no text returns when the caller gives no limit. */
93
+ const LABEL_RECALL_LIMIT = 10;
94
+ /** A labelled text search reads this many hits, so the label filter does not drop a lower-ranked match. */
95
+ const LABEL_SEARCH_POOL = 200;
96
+ /**
97
+ * Fetches memorisations by exact ID, or by text across memorisations only. `tags` keeps only
98
+ * memorisations that carry every label. With `tags` and no text it returns the newest ones.
99
+ * An exact ID ignores `tags`.
100
+ */
101
+ export async function runRecall(args, tags, scope, linked) {
102
+ const input = (args.query ?? args.memoryId ?? args.content)?.trim();
103
+ const wanted = parseTagList(args.tags);
104
+ if (!input && wanted.length === 0) {
105
+ return { success: false, error: "query or tags required: a memorisation ID, text or labels" };
106
+ }
107
+ if (!input)
108
+ return recallByLabels(wanted, tags, scope, args.limit);
109
+ if (isMemorisationId(input)) {
110
+ const found = await getMemorisationById(input, tags.project.tag, linked);
111
+ if (!found)
112
+ return { success: false, error: `No memorisation has the ID ${input}` };
113
+ return { success: true, count: 1, matches: [found] };
114
+ }
115
+ const result = await memoryClient.searchMemories(input, tags.project.tag, scope, {
116
+ includeLinked: true,
117
+ kind: "memorisation",
118
+ ...(wanted.length > 0 ? { limit: LABEL_SEARCH_POOL } : {}),
119
+ });
120
+ if (!result.success)
121
+ return { success: false, error: result.error };
122
+ const candidates = (result.results ?? [])
123
+ .filter((hit) => hasAllTags(hit.tags, wanted))
124
+ .map((hit) => ({
125
+ id: hit.id,
126
+ content: (hit.memory || hit.chunk),
127
+ confidence: Math.round(hit.similarity * 100),
128
+ createdAt: toIso(hit.createdAt),
129
+ tags: visibleTags(hit.tags),
130
+ ...(hit.spaceName ? { space: hit.spaceName } : {}),
131
+ }))
132
+ .sort((a, b) => b.confidence - a.confidence);
133
+ const matches = pickRecallMatches(candidates, args.limit);
134
+ return {
135
+ success: true,
136
+ query: input,
137
+ count: matches.length,
138
+ matches,
139
+ ...(matches.length === 0 ? { message: "No memorisation matched" } : {}),
140
+ };
141
+ }
142
+ /** The newest memorisations that carry every label. They are not ranked, so they have no confidence. */
143
+ async function recallByLabels(wanted, tags, scope, limit) {
144
+ const listed = await listMemorisationsByTags(tags.project.tag, wanted, scope ?? "project", Math.max(1, limit ?? LABEL_RECALL_LIMIT));
145
+ if (!listed.success)
146
+ return listed;
147
+ const matches = listed.memories.map((memory) => ({
148
+ id: memory.id,
149
+ content: memory.summary,
150
+ createdAt: memory.createdAt,
151
+ tags: visibleTags(memory.tags),
152
+ ...(memory.spaceName ? { space: memory.spaceName } : {}),
153
+ }));
154
+ return {
155
+ success: true,
156
+ count: matches.length,
157
+ matches,
158
+ ...(matches.length === 0 ? { message: "No memorisation matched" } : {}),
159
+ };
160
+ }
@@ -0,0 +1,11 @@
1
+ import { type MemorisationSettings } from "./memorisation.js";
2
+ export type { MemorisationSettings } from "./memorisation.js";
3
+ export declare const MEMORISATION_KEYS: readonly ["memorisationMaxSize", "memorisationSizeUnit", "memorisationRepeat"];
4
+ export declare const MEMORISATION_DEFAULTS: MemorisationSettings;
5
+ /** Global-only memorisation settings, with defaults for an older config file. */
6
+ export declare function settingsFrom(config: Partial<MemorisationSettings>): MemorisationSettings;
7
+ /** The web writer validates the pair, so a unit edit cannot strand an invalid number. */
8
+ export declare function checkMemorisationSettingsEdits(edits: Record<string, unknown>, current: MemorisationSettings): void;
9
+ export { settingsFrom as memorisationSettingsFrom };
10
+ /** The three settings are global-only; a project config cannot replace them. */
11
+ export declare function removeMemorisationProjectOverrides(config: Record<string, unknown>): void;
@@ -0,0 +1,48 @@
1
+ import { memorisationCapError, memorisationCapIsValid, } from "./memorisation.js";
2
+ export const MEMORISATION_KEYS = [
3
+ "memorisationMaxSize",
4
+ "memorisationSizeUnit",
5
+ "memorisationRepeat",
6
+ ];
7
+ export const MEMORISATION_DEFAULTS = {
8
+ memorisationMaxSize: 2000,
9
+ memorisationSizeUnit: "characters",
10
+ memorisationRepeat: "add",
11
+ };
12
+ /** Global-only memorisation settings, with defaults for an older config file. */
13
+ export function settingsFrom(config) {
14
+ const memorisationMaxSize = config.memorisationMaxSize ?? MEMORISATION_DEFAULTS.memorisationMaxSize;
15
+ const memorisationSizeUnit = config.memorisationSizeUnit ?? MEMORISATION_DEFAULTS.memorisationSizeUnit;
16
+ if (!memorisationCapIsValid(memorisationMaxSize, memorisationSizeUnit)) {
17
+ throw new Error("Invalid memorisation size settings");
18
+ }
19
+ const memorisationRepeat = config.memorisationRepeat ?? MEMORISATION_DEFAULTS.memorisationRepeat;
20
+ if (memorisationRepeat !== "add" && memorisationRepeat !== "replace") {
21
+ throw new Error("Invalid memorisationRepeat config");
22
+ }
23
+ return { memorisationMaxSize, memorisationSizeUnit, memorisationRepeat };
24
+ }
25
+ /** The web writer validates the pair, so a unit edit cannot strand an invalid number. */
26
+ export function checkMemorisationSettingsEdits(edits, current) {
27
+ if (edits.memorisationRepeat !== undefined) {
28
+ const repeat = edits.memorisationRepeat;
29
+ if (repeat !== "add" && repeat !== "replace") {
30
+ throw new Error("memorisationRepeat must be add or replace");
31
+ }
32
+ }
33
+ const hasSize = Object.hasOwn(edits, "memorisationMaxSize");
34
+ const hasUnit = Object.hasOwn(edits, "memorisationSizeUnit");
35
+ if (!hasSize && !hasUnit)
36
+ return;
37
+ const size = hasSize ? edits.memorisationMaxSize : current.memorisationMaxSize;
38
+ const unit = hasUnit ? edits.memorisationSizeUnit : current.memorisationSizeUnit;
39
+ if (!memorisationCapIsValid(size, unit)) {
40
+ throw new Error(memorisationCapError(unit === "tokens" ? "tokens" : "characters"));
41
+ }
42
+ }
43
+ export { settingsFrom as memorisationSettingsFrom };
44
+ /** The three settings are global-only; a project config cannot replace them. */
45
+ export function removeMemorisationProjectOverrides(config) {
46
+ for (const key of MEMORISATION_KEYS)
47
+ delete config[key];
48
+ }
@@ -0,0 +1,43 @@
1
+ export type MemorisationSizeUnit = "characters" | "tokens";
2
+ export type MemorisationRepeat = "add" | "replace";
3
+ export interface MemorisationSettings {
4
+ memorisationMaxSize: number;
5
+ memorisationSizeUnit: MemorisationSizeUnit;
6
+ memorisationRepeat: MemorisationRepeat;
7
+ }
8
+ export interface MemorisationSnapshot {
9
+ topic?: string;
10
+ done?: string;
11
+ doing?: string;
12
+ next?: string;
13
+ }
14
+ export declare function snapshotText(snapshot: MemorisationSnapshot): string;
15
+ export declare function memorisationId(exists: (id: string) => boolean | Promise<boolean>, random?: () => number): Promise<string>;
16
+ export declare function isMemorisationId(value: string): boolean;
17
+ export declare function memorisationSize(text: string, unit: MemorisationSizeUnit): number;
18
+ export declare function memorisationSizeError(text: string, settings: Pick<MemorisationSettings, "memorisationMaxSize" | "memorisationSizeUnit">): string | null;
19
+ export declare function memorisationSizeRange(unit: MemorisationSizeUnit): {
20
+ min: number;
21
+ max: number;
22
+ };
23
+ export declare function memorisationCapIsValid(maxSize: unknown, unit: unknown): maxSize is number;
24
+ export declare function memorisationCapError(unit: MemorisationSizeUnit): string;
25
+ export interface RecallCandidate {
26
+ id: string;
27
+ content: string;
28
+ confidence: number;
29
+ createdAt?: string;
30
+ space?: string;
31
+ }
32
+ /** One clear winner is enough. Otherwise return three, or the caller's limit when one is given. */
33
+ export declare function pickRecallMatches<T extends {
34
+ confidence: number;
35
+ }>(candidates: T[], limit?: number): T[];
36
+ /** The fixed tag every memorisation carries. It is not a label the user sees. */
37
+ export declare const MEMORISATION_TAG = "memorisation";
38
+ /** A comma-separated tag list as unique, trimmed, lower-case tags in the order given. */
39
+ export declare function parseTagList(text: string | undefined): string[];
40
+ /** The labels of a record: its tags without the fixed memorisation tag, in stored order. */
41
+ export declare function visibleTags(tags: string[] | undefined): string[];
42
+ /** True when the record carries every wanted tag. Case and surrounding spaces do not matter. */
43
+ export declare function hasAllTags(recordTags: string[] | undefined, wanted: string[]): boolean;
@@ -0,0 +1,76 @@
1
+ import { estimateTokens } from "./context-budget.js";
2
+ export function snapshotText(snapshot) {
3
+ return ["topic", "done", "doing", "next"]
4
+ .filter((key) => snapshot[key]?.trim())
5
+ .map((key) => `${key[0].toUpperCase()}${key.slice(1)}: ${snapshot[key].trim()}`)
6
+ .join("\n");
7
+ }
8
+ export function memorisationId(exists, random = Math.random) {
9
+ return (async () => {
10
+ for (let attempt = 0; attempt < 5; attempt++) {
11
+ const suffix = Math.floor(random() * 36 ** 6)
12
+ .toString(36)
13
+ .padStart(6, "0");
14
+ const id = `ms-${suffix}`;
15
+ if (!(await exists(id)))
16
+ return id;
17
+ }
18
+ throw new Error("Could not allocate a unique memorisation ID after five tries");
19
+ })();
20
+ }
21
+ export function isMemorisationId(value) {
22
+ return /^ms-[a-z0-9]{6}$/.test(value.trim());
23
+ }
24
+ export function memorisationSize(text, unit) {
25
+ return unit === "tokens" ? estimateTokens(text) : text.length;
26
+ }
27
+ export function memorisationSizeError(text, settings) {
28
+ const size = memorisationSize(text, settings.memorisationSizeUnit);
29
+ if (size <= settings.memorisationMaxSize)
30
+ return null;
31
+ const unit = settings.memorisationSizeUnit;
32
+ return `Memorisation size limit is ${settings.memorisationMaxSize} ${unit}; the text has ${unit === "tokens" ? "about " : ""}${size} ${unit}. Shorten it and try again.`;
33
+ }
34
+ export function memorisationSizeRange(unit) {
35
+ return unit === "tokens" ? { min: 25, max: 5000 } : { min: 100, max: 20000 };
36
+ }
37
+ export function memorisationCapIsValid(maxSize, unit) {
38
+ return ((unit === "characters" || unit === "tokens") &&
39
+ Number.isSafeInteger(maxSize) &&
40
+ maxSize >= memorisationSizeRange(unit).min &&
41
+ maxSize <= memorisationSizeRange(unit).max);
42
+ }
43
+ export function memorisationCapError(unit) {
44
+ const { min, max } = memorisationSizeRange(unit);
45
+ return `memorisationMaxSize must be a whole number from ${min} to ${max} ${unit}`;
46
+ }
47
+ const DEFAULT_RECALL_MATCHES = 3;
48
+ /** One clear winner is enough. Otherwise return three, or the caller's limit when one is given. */
49
+ export function pickRecallMatches(candidates, limit) {
50
+ const maximum = Math.max(0, limit ?? DEFAULT_RECALL_MATCHES);
51
+ if (candidates.length === 0 || maximum === 0)
52
+ return [];
53
+ const [first, second] = candidates;
54
+ if (first && first.confidence >= 80 && first.confidence - (second?.confidence ?? 0) >= 10) {
55
+ return [first];
56
+ }
57
+ return candidates.slice(0, maximum);
58
+ }
59
+ /** The fixed tag every memorisation carries. It is not a label the user sees. */
60
+ export const MEMORISATION_TAG = "memorisation";
61
+ const normaliseTag = (tag) => tag.trim().toLowerCase();
62
+ /** A comma-separated tag list as unique, trimmed, lower-case tags in the order given. */
63
+ export function parseTagList(text) {
64
+ return [...new Set((text ?? "").split(",").map(normaliseTag).filter(Boolean))];
65
+ }
66
+ /** The labels of a record: its tags without the fixed memorisation tag, in stored order. */
67
+ export function visibleTags(tags) {
68
+ return (tags ?? [])
69
+ .map((tag) => tag.trim())
70
+ .filter((tag) => tag && normaliseTag(tag) !== MEMORISATION_TAG);
71
+ }
72
+ /** True when the record carries every wanted tag. Case and surrounding spaces do not matter. */
73
+ export function hasAllTags(recordTags, wanted) {
74
+ const have = new Set((recordTags ?? []).map(normaliseTag));
75
+ return wanted.every((tag) => have.has(normaliseTag(tag)));
76
+ }
@@ -0,0 +1,11 @@
1
+ import type { CaptureMemoriseState, CaptureToolCall } from "./host.js";
2
+ /** `unknown` for a memorise call (a result still has to answer it), otherwise undefined. */
3
+ export declare function memoriseCallState(name: string, args: unknown): "unknown" | undefined;
4
+ /**
5
+ * The state a result gives a memorise call. A host error, or a JSON reply with
6
+ * `success: false`, is `failed`. The memory tool reports its own failures as a
7
+ * normal reply with `success: false`, so the host does not always mark them.
8
+ */
9
+ export declare function memoriseResultState(isError: boolean, text: string): CaptureMemoriseState;
10
+ /** True when any call in the turn saved a memorisation. */
11
+ export declare function turnSavedMemorisation(toolCalls: readonly CaptureToolCall[]): boolean;