om-memory-system 6.1.1 → 6.2.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/config.js +3 -0
- package/dist/services/ai/provider-error-kind.d.ts +7 -0
- package/dist/services/ai/provider-error-kind.js +9 -0
- package/dist/services/ai/providers/anthropic-messages.js +3 -2
- package/dist/services/ai/providers/base-provider.d.ts +2 -0
- package/dist/services/ai/providers/base-provider.js +4 -0
- package/dist/services/ai/providers/google-gemini.js +3 -2
- package/dist/services/ai/providers/openai-chat-completion.js +3 -2
- package/dist/services/ai/providers/openai-responses.js +3 -2
- package/dist/services/api-handlers.d.ts +3 -24
- package/dist/services/api-handlers.js +4 -114
- package/dist/services/global-config-writer.d.ts +12 -5
- package/dist/services/global-config-writer.js +23 -8
- package/dist/services/openrouter-providers.d.ts +31 -0
- package/dist/services/openrouter-providers.js +124 -0
- package/dist/services/openrouter-routes.d.ts +9 -0
- package/dist/services/openrouter-routes.js +68 -0
- package/dist/services/openrouter-routing-config.d.ts +48 -0
- package/dist/services/openrouter-routing-config.js +143 -0
- package/dist/services/settings-snapshot.d.ts +1 -0
- package/dist/services/settings-snapshot.js +5 -0
- package/dist/services/tag-migration.d.ts +48 -0
- package/dist/services/tag-migration.js +183 -0
- package/dist/services/web-route-modules.js +4 -0
- package/dist/web/assets/index-DgFc9nSc.js +171 -0
- package/dist/web/assets/index-Tc6q6ju1.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CoYuPYCS.css +0 -2
- package/dist/web/assets/index-jbZuE9HV.js +0 -171
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for the OpenRouter routing card: check the posted routing, and
|
|
3
|
+
* turn it into config edits that keep every field the card does not manage.
|
|
4
|
+
*/
|
|
5
|
+
/** The `provider` fields the card owns. Any other field (for example `sort`) is left alone. */
|
|
6
|
+
export declare const MANAGED_PROVIDER_KEYS: readonly ["order", "only", "allow_fallbacks", "data_collection", "zdr"];
|
|
7
|
+
export type RoutingResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
routing: Record<string, unknown>;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
/** Check the posted `routing` object. An empty `order` or `only` list is dropped, since it restricts nothing. */
|
|
15
|
+
export declare function parseRouting(input: unknown): RoutingResult;
|
|
16
|
+
/** True when the URL's host is exactly `openrouter.ai`. Routing applies only to that endpoint. */
|
|
17
|
+
export declare function isOpenRouterUrl(value: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The provider object after the save: the saved object without the five managed
|
|
20
|
+
* fields, plus the posted ones. `undefined` means no provider is left.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeProvider(saved: unknown, routing: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Config edits that remove the whole `provider` object, managed fields or not, and
|
|
25
|
+
* `memoryExtraParams` when nothing else is in it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeProviderEdits(savedExtraParams: unknown): Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Config edits for `memoryExtraParams.provider`. The nested write keeps the other
|
|
30
|
+
* members and the file's comments. When no provider is left and nothing else is in
|
|
31
|
+
* `memoryExtraParams`, the whole key goes. Returns the edits and the new provider.
|
|
32
|
+
*/
|
|
33
|
+
export declare function routingEdits(savedExtraParams: unknown, routing: Record<string, unknown>): {
|
|
34
|
+
edits: Record<string, unknown>;
|
|
35
|
+
provider: Record<string, unknown> | undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Keep the global `memoryExtraParams.provider` when a project file sets `memoryExtraParams`.
|
|
39
|
+
* The project object replaces the global one as a whole, which would drop a privacy choice
|
|
40
|
+
* such as `zdr`. Like the endpoint and the key, the routing is global. A project keeps its
|
|
41
|
+
* other members. Pure on purpose: callers pass the file configs and a log function, and
|
|
42
|
+
* `CONFIG` is never imported here, because many tests stub `src/config.js`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function keepGlobalProvider(globalConfig: {
|
|
45
|
+
memoryExtraParams?: unknown;
|
|
46
|
+
}, projectOverrides: {
|
|
47
|
+
memoryExtraParams?: unknown;
|
|
48
|
+
}, log?: (message: string) => void): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for the OpenRouter routing card: check the posted routing, and
|
|
3
|
+
* turn it into config edits that keep every field the card does not manage.
|
|
4
|
+
*/
|
|
5
|
+
/** The `provider` fields the card owns. Any other field (for example `sort`) is left alone. */
|
|
6
|
+
export const MANAGED_PROVIDER_KEYS = [
|
|
7
|
+
"order",
|
|
8
|
+
"only",
|
|
9
|
+
"allow_fallbacks",
|
|
10
|
+
"data_collection",
|
|
11
|
+
"zdr",
|
|
12
|
+
];
|
|
13
|
+
// A provider slug (`deepinfra`) or a full endpoint slug with up to two more segments, such as
|
|
14
|
+
// `google-vertex/us-east5` or `baseten/fp4/us`. OpenRouter accepts both in `order` and `only`.
|
|
15
|
+
const SEGMENT = "[a-z0-9][a-z0-9._-]{0,63}";
|
|
16
|
+
const SLUG = new RegExp(`^${SEGMENT}(?:/${SEGMENT}){0,2}$`, "i");
|
|
17
|
+
const MAX_SLUGS = 20;
|
|
18
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
function slugList(name, value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return `${name} must be a list of provider slugs`;
|
|
22
|
+
if (value.length > MAX_SLUGS)
|
|
23
|
+
return `${name} can list at most ${MAX_SLUGS} providers`;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const entry of value) {
|
|
26
|
+
if (typeof entry !== "string" || !SLUG.test(entry)) {
|
|
27
|
+
return `${name} has a provider slug that is not valid (letters, digits, dot, dash and underscore, with a slash between parts)`;
|
|
28
|
+
}
|
|
29
|
+
const key = entry.toLowerCase();
|
|
30
|
+
if (seen.has(key))
|
|
31
|
+
return `${name} lists a provider more than once`;
|
|
32
|
+
seen.add(key);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** Check the posted `routing` object. An empty `order` or `only` list is dropped, since it restricts nothing. */
|
|
37
|
+
export function parseRouting(input) {
|
|
38
|
+
if (!isPlainObject(input))
|
|
39
|
+
return { ok: false, error: "routing must be an object" };
|
|
40
|
+
const routing = {};
|
|
41
|
+
for (const [key, value] of Object.entries(input)) {
|
|
42
|
+
if (key === "data_collection") {
|
|
43
|
+
if (value !== "deny")
|
|
44
|
+
return { ok: false, error: 'data_collection can only be "deny"' };
|
|
45
|
+
routing[key] = value;
|
|
46
|
+
}
|
|
47
|
+
else if (key === "zdr") {
|
|
48
|
+
if (value !== true)
|
|
49
|
+
return { ok: false, error: "zdr can only be true" };
|
|
50
|
+
routing[key] = value;
|
|
51
|
+
}
|
|
52
|
+
else if (key === "allow_fallbacks") {
|
|
53
|
+
if (value !== false)
|
|
54
|
+
return { ok: false, error: "allow_fallbacks can only be false" };
|
|
55
|
+
routing[key] = value;
|
|
56
|
+
}
|
|
57
|
+
else if (key === "order" || key === "only") {
|
|
58
|
+
const list = slugList(key, value);
|
|
59
|
+
if (typeof list === "string")
|
|
60
|
+
return { ok: false, error: list };
|
|
61
|
+
if (list.length > 0)
|
|
62
|
+
routing[key] = list;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return { ok: false, error: `routing has a field that is not supported: ${key}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, routing };
|
|
69
|
+
}
|
|
70
|
+
/** True when the URL's host is exactly `openrouter.ai`. Routing applies only to that endpoint. */
|
|
71
|
+
export function isOpenRouterUrl(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
return new URL(value).hostname === "openrouter.ai";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The provider object after the save: the saved object without the five managed
|
|
83
|
+
* fields, plus the posted ones. `undefined` means no provider is left.
|
|
84
|
+
*/
|
|
85
|
+
export function mergeProvider(saved, routing) {
|
|
86
|
+
const merged = isPlainObject(saved) ? { ...saved } : {};
|
|
87
|
+
for (const key of MANAGED_PROVIDER_KEYS)
|
|
88
|
+
delete merged[key];
|
|
89
|
+
Object.assign(merged, routing);
|
|
90
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Config edits that remove the whole `provider` object, managed fields or not, and
|
|
94
|
+
* `memoryExtraParams` when nothing else is in it.
|
|
95
|
+
*/
|
|
96
|
+
export function removeProviderEdits(savedExtraParams) {
|
|
97
|
+
const extra = isPlainObject(savedExtraParams) ? savedExtraParams : undefined;
|
|
98
|
+
const others = extra ? Object.keys(extra).filter((key) => key !== "provider") : [];
|
|
99
|
+
if (!extra || others.length === 0) {
|
|
100
|
+
return savedExtraParams === undefined ? {} : { memoryExtraParams: undefined };
|
|
101
|
+
}
|
|
102
|
+
return { "memoryExtraParams.provider": undefined };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Config edits for `memoryExtraParams.provider`. The nested write keeps the other
|
|
106
|
+
* members and the file's comments. When no provider is left and nothing else is in
|
|
107
|
+
* `memoryExtraParams`, the whole key goes. Returns the edits and the new provider.
|
|
108
|
+
*/
|
|
109
|
+
export function routingEdits(savedExtraParams, routing) {
|
|
110
|
+
const extra = isPlainObject(savedExtraParams) ? savedExtraParams : undefined;
|
|
111
|
+
const provider = mergeProvider(extra?.provider, routing);
|
|
112
|
+
if (provider) {
|
|
113
|
+
// A saved value that is not an object cannot hold a nested key, so it is replaced.
|
|
114
|
+
return {
|
|
115
|
+
edits: extra
|
|
116
|
+
? { "memoryExtraParams.provider": provider }
|
|
117
|
+
: { memoryExtraParams: { provider } },
|
|
118
|
+
provider,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { edits: removeProviderEdits(savedExtraParams), provider };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Keep the global `memoryExtraParams.provider` when a project file sets `memoryExtraParams`.
|
|
125
|
+
* The project object replaces the global one as a whole, which would drop a privacy choice
|
|
126
|
+
* such as `zdr`. Like the endpoint and the key, the routing is global. A project keeps its
|
|
127
|
+
* other members. Pure on purpose: callers pass the file configs and a log function, and
|
|
128
|
+
* `CONFIG` is never imported here, because many tests stub `src/config.js`.
|
|
129
|
+
*/
|
|
130
|
+
export function keepGlobalProvider(globalConfig, projectOverrides, log) {
|
|
131
|
+
const provider = isPlainObject(globalConfig.memoryExtraParams)
|
|
132
|
+
? globalConfig.memoryExtraParams.provider
|
|
133
|
+
: undefined;
|
|
134
|
+
if (!isPlainObject(provider) || !("memoryExtraParams" in projectOverrides))
|
|
135
|
+
return;
|
|
136
|
+
const own = isPlainObject(projectOverrides.memoryExtraParams)
|
|
137
|
+
? projectOverrides.memoryExtraParams
|
|
138
|
+
: {};
|
|
139
|
+
if ("provider" in own) {
|
|
140
|
+
log?.("Project config cannot change memoryExtraParams.provider; the global routing is used");
|
|
141
|
+
}
|
|
142
|
+
projectOverrides.memoryExtraParams = { ...own, provider };
|
|
143
|
+
}
|
|
@@ -18,6 +18,7 @@ export declare function getSettingsSnapshot(directory: string): {
|
|
|
18
18
|
resolvesInWebApp: boolean;
|
|
19
19
|
warning: string | null;
|
|
20
20
|
};
|
|
21
|
+
openrouterProvider: Record<string, unknown> | null;
|
|
21
22
|
effective: {
|
|
22
23
|
opencode: import("./ai/live-model-choice.js").AutoCaptureProviderStatus;
|
|
23
24
|
pi: import("./ai/live-model-choice.js").PiLiveModelChoice;
|
|
@@ -126,6 +126,9 @@ function nestedSetting(path, defaultValue, global, project) {
|
|
|
126
126
|
hasGlobal,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
const plainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
130
|
+
? value
|
|
131
|
+
: null;
|
|
129
132
|
export function getSettingsSnapshot(directory) {
|
|
130
133
|
const global = readSettingsFile(getGlobalConfigSourcePath());
|
|
131
134
|
const project = readSettingsFile(projectFile(directory));
|
|
@@ -218,6 +221,8 @@ export function getSettingsSnapshot(directory) {
|
|
|
218
221
|
secrets,
|
|
219
222
|
fallback: { model: CONFIG.memoryModel ?? null, configured: isExternalModelReady(CONFIG) },
|
|
220
223
|
externalKey: memoryKeyStatus(global.memoryApiKey),
|
|
224
|
+
// The OpenRouter routing card reads the saved `provider` object of memoryExtraParams.
|
|
225
|
+
openrouterProvider: plainObject(plainObject(global.memoryExtraParams)?.provider),
|
|
221
226
|
effective: {
|
|
222
227
|
opencode: getAutoCaptureProviderStatus(CONFIG),
|
|
223
228
|
pi: resolvePiLiveModel(CONFIG),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ProviderErrorKind } from "./ai/provider-error-kind.js";
|
|
2
|
+
import type { buildMemoryProviderConfig } from "./ai/provider-config.js";
|
|
3
|
+
import type { AIProviderType } from "./ai/session/session-types.js";
|
|
4
|
+
export interface ApiResponse<T> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export type TagMigrationConfig = Parameters<typeof buildMemoryProviderConfig>[0] & {
|
|
10
|
+
memoryProvider: AIProviderType;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* One batch's answer. `error` and `errorKind` describe the failure that stopped the run,
|
|
14
|
+
* or else the first failure of the run.
|
|
15
|
+
*/
|
|
16
|
+
export interface TagMigrationBatch {
|
|
17
|
+
processed: number;
|
|
18
|
+
total: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
tagged: number;
|
|
21
|
+
failed: number;
|
|
22
|
+
errorKind?: ProviderErrorKind;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
interface MigrationProgress {
|
|
26
|
+
processed: number;
|
|
27
|
+
total: number;
|
|
28
|
+
currentBatch: number;
|
|
29
|
+
totalBatches: number;
|
|
30
|
+
isComplete: boolean;
|
|
31
|
+
tagged: number;
|
|
32
|
+
errors: string[];
|
|
33
|
+
errorKind?: ProviderErrorKind;
|
|
34
|
+
}
|
|
35
|
+
export declare function handleDetectTagMigration(): Promise<ApiResponse<{
|
|
36
|
+
needsMigration: boolean;
|
|
37
|
+
count: number;
|
|
38
|
+
}>>;
|
|
39
|
+
export declare function handleGetTagMigrationProgress(): Promise<ApiResponse<MigrationProgress>>;
|
|
40
|
+
/**
|
|
41
|
+
* Tag the memories that have no tags, a few per request. Only untagged
|
|
42
|
+
* memories are touched: tagged ones keep their vectors. A memory that fails
|
|
43
|
+
* is recorded and passed over, so one bad row cannot stall the run. An error
|
|
44
|
+
* that fails every memory the same way (no credit, a rejected key) ends the
|
|
45
|
+
* run at once, so a later run starts over with every untagged memory.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runTagMigrationBatch(config: TagMigrationConfig, batchSize?: number): Promise<ApiResponse<TagMigrationBatch>>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI tag backfill for memories saved without tags. Moved out of api-handlers.ts,
|
|
3
|
+
* which sits at its file-size ceiling. The caller passes the config, so this
|
|
4
|
+
* module never imports CONFIG.
|
|
5
|
+
*/
|
|
6
|
+
import { embeddingService } from "./embedding.js";
|
|
7
|
+
import { tursoShardManager } from "./turso/shard-manager.js";
|
|
8
|
+
import { tursoVectorSearch } from "./turso/vector-search.js";
|
|
9
|
+
import { tursoConnectionManager } from "./turso/connection-manager.js";
|
|
10
|
+
import { ensureTursoReady } from "./turso/ready.js";
|
|
11
|
+
import { formatTagsForEmbedding } from "./turso/vector-utils.js";
|
|
12
|
+
import { UNTAGGED_NORMAL_PREDICATE } from "./memory-kind.js";
|
|
13
|
+
import { log } from "./logger.js";
|
|
14
|
+
import { resettable } from "./spaces/store-handles.js";
|
|
15
|
+
import { providerErrorKind } from "./ai/provider-error-kind.js";
|
|
16
|
+
const idleMigration = () => ({
|
|
17
|
+
processed: 0,
|
|
18
|
+
total: 0,
|
|
19
|
+
currentBatch: 0,
|
|
20
|
+
totalBatches: 0,
|
|
21
|
+
isComplete: true,
|
|
22
|
+
tagged: 0,
|
|
23
|
+
errors: [],
|
|
24
|
+
});
|
|
25
|
+
let migrationProgress = idleMigration();
|
|
26
|
+
/** The untagged memories this migration run covers, fixed when the run starts. */
|
|
27
|
+
let migrationQueue = [];
|
|
28
|
+
/** Goes up on a memory space switch, so a batch still running for the old space stops. */
|
|
29
|
+
let spaceGeneration = 0;
|
|
30
|
+
/** The batch now running. Batches run one at a time, so two never take the same memories. */
|
|
31
|
+
let batchInFlight = Promise.resolve();
|
|
32
|
+
// The queue holds paths in the active space, so a space switch drops the run.
|
|
33
|
+
// The next batch then builds a new queue for the new space.
|
|
34
|
+
resettable("tag-migration", null, () => {
|
|
35
|
+
spaceGeneration++;
|
|
36
|
+
migrationProgress = idleMigration();
|
|
37
|
+
});
|
|
38
|
+
/** The longest error text sent to the page. */
|
|
39
|
+
const ERROR_TEXT_LIMIT = 300;
|
|
40
|
+
export async function handleDetectTagMigration() {
|
|
41
|
+
try {
|
|
42
|
+
await ensureTursoReady();
|
|
43
|
+
let untaggedCount = 0;
|
|
44
|
+
for (const shard of await tursoShardManager.getAllShards("project", "")) {
|
|
45
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
46
|
+
const row = await db.get(`SELECT COUNT(*) as count FROM memories WHERE ${UNTAGGED_NORMAL_PREDICATE}`);
|
|
47
|
+
untaggedCount += Number(row?.count ?? 0);
|
|
48
|
+
}
|
|
49
|
+
return { success: true, data: { needsMigration: untaggedCount > 0, count: untaggedCount } };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { success: false, error: String(error) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function handleGetTagMigrationProgress() {
|
|
56
|
+
return { success: true, data: migrationProgress };
|
|
57
|
+
}
|
|
58
|
+
/** The error as the page may show it: short, and with the API key removed. */
|
|
59
|
+
function pageErrorText(error, apiKey) {
|
|
60
|
+
let text = error instanceof Error ? error.message : String(error);
|
|
61
|
+
if (apiKey)
|
|
62
|
+
text = text.split(apiKey).join("[redacted]");
|
|
63
|
+
return text.length > ERROR_TEXT_LIMIT ? `${text.slice(0, ERROR_TEXT_LIMIT)}…` : text;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Tag the memories that have no tags, a few per request. Only untagged
|
|
67
|
+
* memories are touched: tagged ones keep their vectors. A memory that fails
|
|
68
|
+
* is recorded and passed over, so one bad row cannot stall the run. An error
|
|
69
|
+
* that fails every memory the same way (no credit, a rejected key) ends the
|
|
70
|
+
* run at once, so a later run starts over with every untagged memory.
|
|
71
|
+
*/
|
|
72
|
+
export function runTagMigrationBatch(config, batchSize = 5) {
|
|
73
|
+
const run = batchInFlight.then(() => runOneBatch(config, batchSize));
|
|
74
|
+
batchInFlight = run.catch(() => undefined);
|
|
75
|
+
return run;
|
|
76
|
+
}
|
|
77
|
+
async function runOneBatch(config, batchSize) {
|
|
78
|
+
const generation = spaceGeneration;
|
|
79
|
+
try {
|
|
80
|
+
await ensureTursoReady();
|
|
81
|
+
const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
|
|
82
|
+
const { buildMemoryProviderConfig } = await import("./ai/provider-config.js");
|
|
83
|
+
const providerConfig = buildMemoryProviderConfig(config, {
|
|
84
|
+
maxIterations: 1,
|
|
85
|
+
iterationTimeout: 30000,
|
|
86
|
+
});
|
|
87
|
+
const provider = AIProviderFactory.createProvider(config.memoryProvider, providerConfig);
|
|
88
|
+
if (migrationProgress.isComplete) {
|
|
89
|
+
migrationQueue = [];
|
|
90
|
+
for (const shard of await tursoShardManager.getAllShards("project", "")) {
|
|
91
|
+
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
92
|
+
const rows = await db.all(`SELECT id FROM memories WHERE ${UNTAGGED_NORMAL_PREDICATE} ORDER BY id`);
|
|
93
|
+
for (const row of rows)
|
|
94
|
+
migrationQueue.push({ id: String(row.id), dbPath: shard.dbPath });
|
|
95
|
+
}
|
|
96
|
+
migrationProgress = {
|
|
97
|
+
...idleMigration(),
|
|
98
|
+
total: migrationQueue.length,
|
|
99
|
+
totalBatches: Math.ceil(migrationQueue.length / batchSize),
|
|
100
|
+
isComplete: migrationQueue.length === 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const batch = migrationQueue.slice(migrationProgress.processed, migrationProgress.processed + batchSize);
|
|
104
|
+
let stopped = false;
|
|
105
|
+
for (const item of batch) {
|
|
106
|
+
if (generation !== spaceGeneration) {
|
|
107
|
+
return { success: false, error: "The memory space changed during tagging" };
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const db = await tursoConnectionManager.getConnection(item.dbPath);
|
|
111
|
+
const m = await db.get("SELECT * FROM memories WHERE id = ?", [item.id]);
|
|
112
|
+
// Tagged or deleted since the run started: nothing to do.
|
|
113
|
+
if (!m || (m.tags && String(m.tags).trim()))
|
|
114
|
+
continue;
|
|
115
|
+
const prompt = `Generate 2-4 short technical tags for this memory content:\n\n${m.content}\n\nReturn ONLY a comma-separated list of tags.`;
|
|
116
|
+
const result = await provider.executeToolCall("You are a technical tagger.", prompt, {
|
|
117
|
+
type: "function",
|
|
118
|
+
function: {
|
|
119
|
+
name: "save_tags",
|
|
120
|
+
description: "Save generated tags",
|
|
121
|
+
parameters: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: { tags: { type: "array", items: { type: "string" } } },
|
|
124
|
+
required: ["tags"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}, `migration_${m.id}`);
|
|
128
|
+
const tags = Array.isArray(result.data?.tags)
|
|
129
|
+
? result.data.tags.map((t) => String(t).trim().toLowerCase()).filter(Boolean)
|
|
130
|
+
: [];
|
|
131
|
+
if (!result.success || tags.length === 0) {
|
|
132
|
+
throw new Error(result.error ?? "The model returned no tags");
|
|
133
|
+
}
|
|
134
|
+
const vector = await embeddingService.embedWithTimeout(m.content, { task: "document" });
|
|
135
|
+
const tagsVector = await embeddingService.embedWithTimeout(formatTagsForEmbedding(tags), {
|
|
136
|
+
task: "document",
|
|
137
|
+
});
|
|
138
|
+
// One statement, so a memory never ends up tagged with stale vectors.
|
|
139
|
+
await tursoVectorSearch.updateVector(db, m.id, vector, tagsVector, tags.join(","));
|
|
140
|
+
migrationProgress.tagged++;
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
const errorMsg = pageErrorText(e, config.memoryApiKey);
|
|
144
|
+
const kind = providerErrorKind(errorMsg);
|
|
145
|
+
log("Migration error for memory", { id: item.id, error: errorMsg, kind }, "error");
|
|
146
|
+
if (kind === "other") {
|
|
147
|
+
migrationProgress.errors.push(errorMsg);
|
|
148
|
+
migrationProgress.errorKind ??= kind;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// This error ends the run, so it is the one the page reports.
|
|
152
|
+
migrationProgress.errors.unshift(errorMsg);
|
|
153
|
+
migrationProgress.errorKind = kind;
|
|
154
|
+
stopped = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
migrationProgress.processed++;
|
|
159
|
+
}
|
|
160
|
+
if (stopped)
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
migrationProgress.currentBatch++;
|
|
164
|
+
const hasMore = !stopped && migrationProgress.processed < migrationProgress.total;
|
|
165
|
+
if (!hasMore)
|
|
166
|
+
migrationProgress.isComplete = true;
|
|
167
|
+
const { processed, total, tagged, errors, errorKind } = migrationProgress;
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
data: {
|
|
171
|
+
processed,
|
|
172
|
+
total,
|
|
173
|
+
hasMore,
|
|
174
|
+
tagged,
|
|
175
|
+
failed: errors.length,
|
|
176
|
+
...(errors.length > 0 ? { errorKind, error: errors[0] } : {}),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return { success: false, error: String(error) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -2,6 +2,7 @@ import { routeChat } from "./chat/chat-routes.js";
|
|
|
2
2
|
import { linkedSettingsFrom } from "./linked-search.js";
|
|
3
3
|
import { routeMemorisations } from "./memorisation-routes.js";
|
|
4
4
|
import { routeMemoryHosts } from "./memory-host-routes.js";
|
|
5
|
+
import { routeOpenRouter } from "./openrouter-routes.js";
|
|
5
6
|
import { routeProfileCleanup } from "./profile-cleanup-routes.js";
|
|
6
7
|
import { routeSpaceTransfers } from "./space-transfer-routes.js";
|
|
7
8
|
import { routeSpaces } from "./space-routes.js";
|
|
@@ -21,6 +22,9 @@ export async function routeModules(path, method, url, req, ctx) {
|
|
|
21
22
|
const hosts = await routeMemoryHosts(path, method, url, readBody, config);
|
|
22
23
|
if (hosts !== undefined)
|
|
23
24
|
return hosts;
|
|
25
|
+
const openRouter = await routeOpenRouter(path, method, url, readBody, ctx);
|
|
26
|
+
if (openRouter !== undefined)
|
|
27
|
+
return openRouter;
|
|
24
28
|
if (path === "/api/skills" || path.startsWith("/api/skills/")) {
|
|
25
29
|
const { routeSkills } = await import("./skill-routes.js");
|
|
26
30
|
return routeSkills(path, method, url, readBody, ctx);
|