om-memory-system 6.3.0 → 6.5.0
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/core/memorisation-operations.d.ts +2 -2
- package/dist/core/memorisation-operations.js +5 -5
- package/dist/core/memorisation.d.ts +16 -2
- package/dist/core/memorisation.js +41 -3
- package/dist/core/memory-operations.js +1 -1
- package/dist/services/memorisation-routes.d.ts +1 -1
- package/dist/services/memorisation-routes.js +11 -4
- package/dist/services/memorisation-store.d.ts +13 -2
- package/dist/services/memorisation-store.js +13 -3
- package/dist/web/assets/index-BDT_CO7C.js +171 -0
- package/dist/web/assets/index-BnYa8Wb_.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-4epIq1xy.js +0 -171
- package/dist/web/assets/index-Tc6q6ju1.css +0 -2
|
@@ -12,7 +12,7 @@ export interface RecallArgs {
|
|
|
12
12
|
/** Also read as an ID or text when `query` is empty. */
|
|
13
13
|
memoryId?: string;
|
|
14
14
|
content?: string;
|
|
15
|
-
/**
|
|
15
|
+
/** Labels. A comma means AND and a bar means OR: `server | reactjs, api` needs api and either of the others. */
|
|
16
16
|
tags?: string;
|
|
17
17
|
limit?: number;
|
|
18
18
|
}
|
|
@@ -24,7 +24,7 @@ interface MemorisationContext {
|
|
|
24
24
|
export declare function runMemorise(args: MemoriseArgs, context: MemorisationContext, tags: ReturnType<typeof getTags>, settings: MemorisationSettings, linked: LinkedSearchSettings): Promise<Record<string, unknown>>;
|
|
25
25
|
/**
|
|
26
26
|
* Fetches memorisations by exact ID, or by text across memorisations only. `tags` keeps only
|
|
27
|
-
* memorisations that
|
|
27
|
+
* memorisations that match every label group. With `tags` and no text it returns the newest ones.
|
|
28
28
|
* An exact ID ignores `tags`. Every read covers the whole active space and its linked spaces,
|
|
29
29
|
* so a memorisation is found from any folder. `rerank` (the `memorisationRerank` setting)
|
|
30
30
|
* lets the reranker server reorder the text matches; each match keeps its content-only
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memoryClient } from "../services/client.js";
|
|
2
2
|
import { findSnapshotByHostSession, getMemorisationById, listMemorisationsByTags, memorisationIdTaken, } from "../services/memorisation-store.js";
|
|
3
3
|
import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
|
|
4
|
-
import {
|
|
4
|
+
import { matchesTagGroups, isMemorisationId, MEMORISATION_TAG, memorisationId, memorisationSizeError, parseTagGroups, parseTagList, pickRecallMatches, snapshotText, visibleTags, withProjectLabel, } from "./memorisation.js";
|
|
5
5
|
const SNAPSHOT_FIELDS = ["topic", "done", "doing", "next"];
|
|
6
6
|
const BLOCKED = { success: false, error: "Private content blocked" };
|
|
7
7
|
/** Strips `<private>` text. A field that is fully private is dropped. */
|
|
@@ -97,7 +97,7 @@ const LABEL_RECALL_LIMIT = 10;
|
|
|
97
97
|
const LABEL_SEARCH_POOL = 200;
|
|
98
98
|
/**
|
|
99
99
|
* Fetches memorisations by exact ID, or by text across memorisations only. `tags` keeps only
|
|
100
|
-
* memorisations that
|
|
100
|
+
* memorisations that match every label group. With `tags` and no text it returns the newest ones.
|
|
101
101
|
* An exact ID ignores `tags`. Every read covers the whole active space and its linked spaces,
|
|
102
102
|
* so a memorisation is found from any folder. `rerank` (the `memorisationRerank` setting)
|
|
103
103
|
* lets the reranker server reorder the text matches; each match keeps its content-only
|
|
@@ -105,7 +105,7 @@ const LABEL_SEARCH_POOL = 200;
|
|
|
105
105
|
*/
|
|
106
106
|
export async function runRecall(args, linked, rerank = false) {
|
|
107
107
|
const input = (args.query ?? args.memoryId ?? args.content)?.trim();
|
|
108
|
-
const wanted =
|
|
108
|
+
const wanted = parseTagGroups(args.tags);
|
|
109
109
|
if (!input && wanted.length === 0) {
|
|
110
110
|
return { success: false, error: "query or tags required: a memorisation ID, text or labels" };
|
|
111
111
|
}
|
|
@@ -128,7 +128,7 @@ export async function runRecall(args, linked, rerank = false) {
|
|
|
128
128
|
if (!result.success)
|
|
129
129
|
return { success: false, error: result.error };
|
|
130
130
|
const mapped = (result.results ?? [])
|
|
131
|
-
.filter((hit) =>
|
|
131
|
+
.filter((hit) => matchesTagGroups(hit.tags, wanted))
|
|
132
132
|
.map((hit) => ({
|
|
133
133
|
id: hit.id,
|
|
134
134
|
content: (hit.memory || hit.chunk),
|
|
@@ -149,7 +149,7 @@ export async function runRecall(args, linked, rerank = false) {
|
|
|
149
149
|
...(matches.length === 0 ? { message: "No memorisation matched" } : {}),
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
/** The newest memorisations that
|
|
152
|
+
/** The newest memorisations that match every label group. They are not ranked, so they have no confidence. */
|
|
153
153
|
async function recallByLabels(wanted, limit) {
|
|
154
154
|
const listed = await listMemorisationsByTags("", wanted, "all-projects", Math.max(1, limit ?? LABEL_RECALL_LIMIT));
|
|
155
155
|
if (!listed.success)
|
|
@@ -40,8 +40,22 @@ export declare const MEMORISATION_TAG = "memorisation";
|
|
|
40
40
|
export declare function parseTagList(text: string | undefined): string[];
|
|
41
41
|
/** The labels of a record: its tags without the fixed memorisation tag, in stored order. */
|
|
42
42
|
export declare function visibleTags(tags: string[] | undefined): string[];
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The labels in use with the number of records that carry each. Labels that differ only in
|
|
45
|
+
* letter case or surrounding spaces count as one, shown in the spelling seen first. A record
|
|
46
|
+
* counts once per label. The fixed memorisation tag is left out. Most records come first, then name.
|
|
47
|
+
*/
|
|
48
|
+
export declare function countLabels(recordTags: ReadonlyArray<string[] | undefined>): {
|
|
49
|
+
label: string;
|
|
50
|
+
count: number;
|
|
51
|
+
}[];
|
|
52
|
+
/**
|
|
53
|
+
* A recall label filter as groups. A comma means AND and a bar means OR, so `server | reactjs, api`
|
|
54
|
+
* is `[["server", "reactjs"], ["api"]]`: a record needs api and one of server or reactjs.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseTagGroups(text: string | undefined): string[][];
|
|
57
|
+
/** True when each group has at least one label the record carries. Case and surrounding spaces do not matter. */
|
|
58
|
+
export declare function matchesTagGroups(recordTags: string[] | undefined, groups: string[][]): boolean;
|
|
45
59
|
/**
|
|
46
60
|
* The project name as one more label, after the fixed tag and the caller's labels. The root
|
|
47
61
|
* folder name `/` is not a usable label, so a caller there gets no project label.
|
|
@@ -69,10 +69,48 @@ export function visibleTags(tags) {
|
|
|
69
69
|
.map((tag) => tag.trim())
|
|
70
70
|
.filter((tag) => tag && normaliseTag(tag) !== MEMORISATION_TAG);
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The labels in use with the number of records that carry each. Labels that differ only in
|
|
74
|
+
* letter case or surrounding spaces count as one, shown in the spelling seen first. A record
|
|
75
|
+
* counts once per label. The fixed memorisation tag is left out. Most records come first, then name.
|
|
76
|
+
*/
|
|
77
|
+
export function countLabels(recordTags) {
|
|
78
|
+
const counts = new Map();
|
|
79
|
+
for (const tags of recordTags) {
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const label of visibleTags(tags)) {
|
|
82
|
+
const key = normaliseTag(label);
|
|
83
|
+
if (seen.has(key))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
const own = counts.get(key);
|
|
87
|
+
if (own)
|
|
88
|
+
own.count += 1;
|
|
89
|
+
else
|
|
90
|
+
counts.set(key, { label, count: 1 });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...counts.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A recall label filter as groups. A comma means AND and a bar means OR, so `server | reactjs, api`
|
|
97
|
+
* is `[["server", "reactjs"], ["api"]]`: a record needs api and one of server or reactjs.
|
|
98
|
+
*/
|
|
99
|
+
export function parseTagGroups(text) {
|
|
100
|
+
const groups = (text ?? "")
|
|
101
|
+
.split(",")
|
|
102
|
+
.map((part) => [...new Set(part.split("|").map(normaliseTag).filter(Boolean))])
|
|
103
|
+
.filter((group) => group.length > 0);
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return groups.filter((group) => {
|
|
106
|
+
const key = [...group].sort().join("|");
|
|
107
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** True when each group has at least one label the record carries. Case and surrounding spaces do not matter. */
|
|
111
|
+
export function matchesTagGroups(recordTags, groups) {
|
|
74
112
|
const have = new Set((recordTags ?? []).map(normaliseTag));
|
|
75
|
-
return
|
|
113
|
+
return groups.every((group) => group.some((tag) => have.has(normaliseTag(tag))));
|
|
76
114
|
}
|
|
77
115
|
/**
|
|
78
116
|
* The project name as one more label, after the fixed tag and the caller's labels. The root
|
|
@@ -85,7 +85,7 @@ export async function executeMemoryOperation(args, context) {
|
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
command: "recall",
|
|
88
|
-
description: "Fetch memorisations only: pass an ms- ID for the exact text, or text for ranked matches with a confidence score. Pass tags (the labels) to keep only memorisations that carry all of them; tags with no query lists the newest ones",
|
|
88
|
+
description: "Fetch memorisations only: pass an ms- ID for the exact text, or text for ranked matches with a confidence score. Pass tags (the labels) to keep only memorisations that carry all of them (comma = AND); write a|b for either label (bar = OR), such as server|reactjs,api; tags with no query lists the newest ones",
|
|
89
89
|
args: ["query?", "tags?", "limit?"],
|
|
90
90
|
},
|
|
91
91
|
{
|
|
@@ -8,7 +8,7 @@ export interface MemorisationRouteConfig {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Routes for the Memorisation page. Returns undefined for paths it does not own.
|
|
11
|
-
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
11
|
+
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/labels` counts the labels, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
12
12
|
* and `POST /api/memorisations/bulk-delete` deletes. Every read covers the whole active
|
|
13
13
|
* space and its linked spaces, so the page works from any folder. The general `/api/memories`
|
|
14
14
|
* delete routes refuse memorisations by design, so this module owns the bulk delete.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isMemorisationId,
|
|
1
|
+
import { isMemorisationId, parseTagGroups, visibleTags } from "../core/memorisation.js";
|
|
2
2
|
/** Enough for any project: a memorisation is capped in size, and the user saves each on purpose. */
|
|
3
3
|
const LIST_LIMIT = 200;
|
|
4
4
|
/** The most IDs one bulk delete takes. */
|
|
@@ -10,16 +10,18 @@ const fail = (status, error) => ({
|
|
|
10
10
|
});
|
|
11
11
|
/**
|
|
12
12
|
* Routes for the Memorisation page. Returns undefined for paths it does not own.
|
|
13
|
-
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
13
|
+
* `GET /api/memorisations?tags=a,b` lists, `GET /api/memorisations/labels` counts the labels, `GET /api/memorisations/recall?q=&tags=` recalls
|
|
14
14
|
* and `POST /api/memorisations/bulk-delete` deletes. Every read covers the whole active
|
|
15
15
|
* space and its linked spaces, so the page works from any folder. The general `/api/memories`
|
|
16
16
|
* delete routes refuse memorisations by design, so this module owns the bulk delete.
|
|
17
17
|
* Memorisation text is returned to the page and never written to the log.
|
|
18
18
|
*/
|
|
19
19
|
export async function routeMemorisations(path, method, url, config, readBody) {
|
|
20
|
-
const wanted =
|
|
20
|
+
const wanted = parseTagGroups(url.searchParams.get("tags") ?? undefined);
|
|
21
21
|
if (method === "GET" && path === "/api/memorisations")
|
|
22
22
|
return listMemorisations(wanted);
|
|
23
|
+
if (method === "GET" && path === "/api/memorisations/labels")
|
|
24
|
+
return listLabels();
|
|
23
25
|
if (method === "GET" && path === "/api/memorisations/recall") {
|
|
24
26
|
return recall(url.searchParams, wanted, config);
|
|
25
27
|
}
|
|
@@ -44,6 +46,11 @@ async function listMemorisations(wanted) {
|
|
|
44
46
|
}));
|
|
45
47
|
return ok({ memorisations });
|
|
46
48
|
}
|
|
49
|
+
async function listLabels() {
|
|
50
|
+
const { listMemorisationLabels } = await import("./memorisation-store.js");
|
|
51
|
+
const result = await listMemorisationLabels();
|
|
52
|
+
return result.success ? ok({ labels: result.labels }) : fail(500, result.error);
|
|
53
|
+
}
|
|
47
54
|
async function recall(params, wanted, config) {
|
|
48
55
|
const text = params.get("q")?.trim();
|
|
49
56
|
if (!text && wanted.length === 0)
|
|
@@ -52,7 +59,7 @@ async function recall(params, wanted, config) {
|
|
|
52
59
|
const { runRecall } = await import("../core/memorisation-operations.js");
|
|
53
60
|
const result = await runRecall({
|
|
54
61
|
query: text,
|
|
55
|
-
tags: wanted.join(","),
|
|
62
|
+
tags: wanted.map((group) => group.join("|")).join(","),
|
|
56
63
|
...(Number.isInteger(limit) && limit > 0 ? { limit } : {}),
|
|
57
64
|
}, config.linked, config.rerank);
|
|
58
65
|
if (result.success !== true) {
|
|
@@ -26,17 +26,28 @@ export declare function findSnapshotByHostSession(hostSessionId: string, contain
|
|
|
26
26
|
id: string;
|
|
27
27
|
} | null>;
|
|
28
28
|
/**
|
|
29
|
-
* The project's memorisations that
|
|
29
|
+
* The project's memorisations that match every group in `tags` (a comma is AND, a bar is OR), newest first, in the active
|
|
30
30
|
* space and its linked spaces. The tag filter runs before `limit`, so an old labelled
|
|
31
31
|
* row is not pushed out by newer rows without the label.
|
|
32
32
|
*/
|
|
33
|
-
export declare function listMemorisationsByTags(containerTag: string, tags: string[], scope: MemoryScope, limit: number): Promise<{
|
|
33
|
+
export declare function listMemorisationsByTags(containerTag: string, tags: string[][], scope: MemoryScope, limit: number): Promise<{
|
|
34
34
|
success: true;
|
|
35
35
|
memories: ListedMemory[];
|
|
36
36
|
} | {
|
|
37
37
|
success: false;
|
|
38
38
|
error: string;
|
|
39
39
|
}>;
|
|
40
|
+
/** Every label in use across the active space and its linked spaces, with its record count. */
|
|
41
|
+
export declare function listMemorisationLabels(): Promise<{
|
|
42
|
+
success: true;
|
|
43
|
+
labels: {
|
|
44
|
+
label: string;
|
|
45
|
+
count: number;
|
|
46
|
+
}[];
|
|
47
|
+
} | {
|
|
48
|
+
success: false;
|
|
49
|
+
error: string;
|
|
50
|
+
}>;
|
|
40
51
|
export type MemorisationDeleteOutcome = "deleted" | "not-found" | "linked" | "not-memorisation";
|
|
41
52
|
/**
|
|
42
53
|
* Deletes one memorisation from the active space. A project tag narrows the delete to that
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { countLabels, matchesTagGroups, visibleTags } from "../core/memorisation.js";
|
|
2
2
|
import { activeOwner } from "./active-space-access.js";
|
|
3
3
|
import { readLinkedMemory } from "./linked-search.js";
|
|
4
4
|
import { extractScopeFromContainerTag } from "./memory-scope.js";
|
|
@@ -78,7 +78,7 @@ export async function findSnapshotByHostSession(hostSessionId, containerTag) {
|
|
|
78
78
|
/** Far above any real project: a memorisation is capped in size and the user saves each on purpose. */
|
|
79
79
|
const READ_CEILING = 10_000;
|
|
80
80
|
/**
|
|
81
|
-
* The project's memorisations that
|
|
81
|
+
* The project's memorisations that match every group in `tags` (a comma is AND, a bar is OR), newest first, in the active
|
|
82
82
|
* space and its linked spaces. The tag filter runs before `limit`, so an old labelled
|
|
83
83
|
* row is not pushed out by newer rows without the label.
|
|
84
84
|
*/
|
|
@@ -90,12 +90,22 @@ export async function listMemorisationsByTags(containerTag, tags, scope, limit)
|
|
|
90
90
|
if (!result.success)
|
|
91
91
|
return { success: false, error: result.error };
|
|
92
92
|
const memories = result.memories
|
|
93
|
-
.filter((memory) =>
|
|
93
|
+
.filter((memory) => matchesTagGroups(memory.tags, tags))
|
|
94
94
|
// Own rows come first from the client, then linked rows. Callers want one timeline.
|
|
95
95
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
96
96
|
.slice(0, limit);
|
|
97
97
|
return { success: true, memories };
|
|
98
98
|
}
|
|
99
|
+
/** Every label in use across the active space and its linked spaces, with its record count. */
|
|
100
|
+
export async function listMemorisationLabels() {
|
|
101
|
+
const result = await memoryClient.listMemories("", READ_CEILING, "all-projects", {
|
|
102
|
+
includeLinked: true,
|
|
103
|
+
kind: "memorisation",
|
|
104
|
+
});
|
|
105
|
+
if (!result.success)
|
|
106
|
+
return { success: false, error: result.error };
|
|
107
|
+
return { success: true, labels: countLabels(result.memories.map((memory) => memory.tags)) };
|
|
108
|
+
}
|
|
99
109
|
/**
|
|
100
110
|
* Deletes one memorisation from the active space. A project tag narrows the delete to that
|
|
101
111
|
* project; `null` matches any project, the rule bulk delete uses. No row is "not-found".
|