@hasna/mementos 0.14.81 → 0.14.82
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/cli/commands/memory-cmd-crud.d.ts.map +1 -1
- package/dist/cli/commands/project.d.ts.map +1 -1
- package/dist/cli/index.js +547 -72
- package/dist/db/memory-project-link.d.ts.map +1 -1
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/index.js +460 -81
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/identity.d.ts +20 -0
- package/dist/project-registration/identity.d.ts.map +1 -0
- package/dist/project-registration/index.d.ts +5 -2
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/project-resources.d.ts +22 -0
- package/dist/project-registration/project-resources.d.ts.map +1 -0
- package/dist/project-registration/types.d.ts +52 -0
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +1488 -55
- package/dist/sdk/index.d.ts +57 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +56 -0
- package/dist/server/index.js +644 -145
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/test-support/project-authority-identity.d.ts +8 -0
- package/dist/test-support/project-authority-identity.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -965,7 +965,7 @@ var init_storage = __esm(() => {
|
|
|
965
965
|
|
|
966
966
|
// src/db/api-mode.ts
|
|
967
967
|
import { tmpdir } from "os";
|
|
968
|
-
import { join as
|
|
968
|
+
import { join as join2 } from "path";
|
|
969
969
|
import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
970
970
|
import { randomUUID } from "crypto";
|
|
971
971
|
function firstEnv(keys) {
|
|
@@ -1074,7 +1074,7 @@ x-api-key: ${cfg.apiKey}
|
|
|
1074
1074
|
];
|
|
1075
1075
|
let bodyFile;
|
|
1076
1076
|
if (hasBody) {
|
|
1077
|
-
bodyFile =
|
|
1077
|
+
bodyFile = join2(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
|
|
1078
1078
|
writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
|
|
1079
1079
|
args.push("--data-binary", `@${bodyFile}`);
|
|
1080
1080
|
}
|
|
@@ -2585,7 +2585,7 @@ __export(exports_database, {
|
|
|
2585
2585
|
closeDatabase: () => closeDatabase
|
|
2586
2586
|
});
|
|
2587
2587
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
2588
|
-
import { dirname
|
|
2588
|
+
import { dirname, join as join3, resolve } from "path";
|
|
2589
2589
|
function isInMemoryDb(path) {
|
|
2590
2590
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
2591
2591
|
}
|
|
@@ -2594,10 +2594,10 @@ function findNearestMementosDb(startDir) {
|
|
|
2594
2594
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2595
2595
|
const legacyHomeDb = resolve(home, ".mementos", "mementos.db");
|
|
2596
2596
|
while (true) {
|
|
2597
|
-
const candidate =
|
|
2597
|
+
const candidate = join3(dir, ".mementos", "mementos.db");
|
|
2598
2598
|
if (existsSync2(candidate) && resolve(candidate) !== legacyHomeDb)
|
|
2599
2599
|
return candidate;
|
|
2600
|
-
const parent =
|
|
2600
|
+
const parent = dirname(dir);
|
|
2601
2601
|
if (parent === dir)
|
|
2602
2602
|
break;
|
|
2603
2603
|
dir = parent;
|
|
@@ -2607,9 +2607,9 @@ function findNearestMementosDb(startDir) {
|
|
|
2607
2607
|
function findGitRoot(startDir) {
|
|
2608
2608
|
let dir = resolve(startDir);
|
|
2609
2609
|
while (true) {
|
|
2610
|
-
if (existsSync2(
|
|
2610
|
+
if (existsSync2(join3(dir, ".git")))
|
|
2611
2611
|
return dir;
|
|
2612
|
-
const parent =
|
|
2612
|
+
const parent = dirname(dir);
|
|
2613
2613
|
if (parent === dir)
|
|
2614
2614
|
break;
|
|
2615
2615
|
dir = parent;
|
|
@@ -2618,10 +2618,10 @@ function findGitRoot(startDir) {
|
|
|
2618
2618
|
}
|
|
2619
2619
|
function migrateGlobalDir() {
|
|
2620
2620
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2621
|
-
const newDir =
|
|
2622
|
-
const oldDir =
|
|
2621
|
+
const newDir = join3(home, ".hasna", "mementos");
|
|
2622
|
+
const oldDir = join3(home, ".mementos");
|
|
2623
2623
|
if (!existsSync2(newDir) && existsSync2(oldDir)) {
|
|
2624
|
-
mkdirSync2(
|
|
2624
|
+
mkdirSync2(join3(home, ".hasna"), { recursive: true });
|
|
2625
2625
|
cpSync(oldDir, newDir, { recursive: true });
|
|
2626
2626
|
}
|
|
2627
2627
|
}
|
|
@@ -2638,17 +2638,17 @@ function getDbPath() {
|
|
|
2638
2638
|
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
2639
2639
|
const gitRoot = findGitRoot(cwd);
|
|
2640
2640
|
if (gitRoot) {
|
|
2641
|
-
return
|
|
2641
|
+
return join3(gitRoot, ".mementos", "mementos.db");
|
|
2642
2642
|
}
|
|
2643
2643
|
}
|
|
2644
2644
|
migrateGlobalDir();
|
|
2645
2645
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2646
|
-
return
|
|
2646
|
+
return join3(home, ".hasna", "mementos", "mementos.db");
|
|
2647
2647
|
}
|
|
2648
2648
|
function ensureDir(filePath) {
|
|
2649
2649
|
if (isInMemoryDb(filePath))
|
|
2650
2650
|
return;
|
|
2651
|
-
const dir =
|
|
2651
|
+
const dir = dirname(resolve(filePath));
|
|
2652
2652
|
if (!existsSync2(dir)) {
|
|
2653
2653
|
mkdirSync2(dir, { recursive: true });
|
|
2654
2654
|
}
|
|
@@ -2808,15 +2808,20 @@ var init_database = __esm(() => {
|
|
|
2808
2808
|
import { createHash as createHash2 } from "crypto";
|
|
2809
2809
|
import { resolve as resolve2 } from "path";
|
|
2810
2810
|
|
|
2811
|
+
// src/db/projects.ts
|
|
2812
|
+
init_database();
|
|
2813
|
+
init_api_mode();
|
|
2814
|
+
import { createHash } from "crypto";
|
|
2815
|
+
|
|
2811
2816
|
// src/lib/package-version.ts
|
|
2812
2817
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2813
|
-
import { dirname, join as
|
|
2818
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2814
2819
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2815
2820
|
function getMementosPackageVersion() {
|
|
2816
|
-
const here =
|
|
2821
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
2817
2822
|
for (const candidate of [
|
|
2818
|
-
|
|
2819
|
-
|
|
2823
|
+
join4(here, "..", "..", "package.json"),
|
|
2824
|
+
join4(here, "..", "package.json")
|
|
2820
2825
|
]) {
|
|
2821
2826
|
try {
|
|
2822
2827
|
const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
|
|
@@ -2827,16 +2832,18 @@ function getMementosPackageVersion() {
|
|
|
2827
2832
|
return "0.0.0";
|
|
2828
2833
|
}
|
|
2829
2834
|
|
|
2830
|
-
// src/db/projects.ts
|
|
2831
|
-
init_database();
|
|
2832
|
-
init_api_mode();
|
|
2833
|
-
import { createHash } from "crypto";
|
|
2834
|
-
|
|
2835
2835
|
// src/project-registration/types.ts
|
|
2836
2836
|
var MEMENTOS_PROJECT_REGISTRATION_ROUTE = "mementos.project-registration.v1";
|
|
2837
2837
|
var MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
2838
2838
|
var MEMENTOS_PROJECT_REGISTRATION_SCHEMA_VERSION = 1;
|
|
2839
2839
|
var MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE = "mementos.project-guarded-update.v1";
|
|
2840
|
+
var MEMENTOS_PROJECT_RESOURCE_ROUTE = "mementos.project-resources.v1";
|
|
2841
|
+
var MEMENTOS_PROJECT_RESOURCE_KINDS = [
|
|
2842
|
+
"project",
|
|
2843
|
+
"knowledge",
|
|
2844
|
+
"memory",
|
|
2845
|
+
"session"
|
|
2846
|
+
];
|
|
2840
2847
|
|
|
2841
2848
|
class MementosProjectRegistrationError extends Error {
|
|
2842
2849
|
code;
|
|
@@ -2849,6 +2856,73 @@ class MementosProjectRegistrationError extends Error {
|
|
|
2849
2856
|
}
|
|
2850
2857
|
}
|
|
2851
2858
|
|
|
2859
|
+
// src/project-registration/identity.ts
|
|
2860
|
+
var MEMENTOS_PROJECT_AUTHORITY_ENV = {
|
|
2861
|
+
authorityId: "MEMENTOS_PROJECT_AUTHORITY_ID",
|
|
2862
|
+
tenantId: "MEMENTOS_PROJECT_TENANT_ID",
|
|
2863
|
+
corpusId: "MEMENTOS_PROJECT_CORPUS_ID"
|
|
2864
|
+
};
|
|
2865
|
+
function configuredValue(override, envKey) {
|
|
2866
|
+
return override?.trim() || process.env[envKey]?.trim() || null;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
class MementosProjectAuthorityIdentityError extends Error {
|
|
2870
|
+
missing_env;
|
|
2871
|
+
code = "MEMENTOS_PROJECT_AUTHORITY_UNCONFIGURED";
|
|
2872
|
+
constructor(missing_env) {
|
|
2873
|
+
super("Mementos project authority identity is not configured; set " + missing_env.join(", "));
|
|
2874
|
+
this.missing_env = missing_env;
|
|
2875
|
+
this.name = "MementosProjectAuthorityIdentityError";
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function resolveMementosProjectAuthorityIdentity(options = {}) {
|
|
2879
|
+
const authorityId = configuredValue(options.authorityId, MEMENTOS_PROJECT_AUTHORITY_ENV.authorityId);
|
|
2880
|
+
const tenantId = configuredValue(options.tenantId, MEMENTOS_PROJECT_AUTHORITY_ENV.tenantId);
|
|
2881
|
+
const corpusId = configuredValue(options.corpusId, MEMENTOS_PROJECT_AUTHORITY_ENV.corpusId);
|
|
2882
|
+
if (!authorityId || !tenantId || !corpusId) {
|
|
2883
|
+
const missingEnv = [];
|
|
2884
|
+
if (!authorityId)
|
|
2885
|
+
missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.authorityId);
|
|
2886
|
+
if (!tenantId)
|
|
2887
|
+
missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.tenantId);
|
|
2888
|
+
if (!corpusId)
|
|
2889
|
+
missingEnv.push(MEMENTOS_PROJECT_AUTHORITY_ENV.corpusId);
|
|
2890
|
+
throw new MementosProjectAuthorityIdentityError(missingEnv);
|
|
2891
|
+
}
|
|
2892
|
+
return {
|
|
2893
|
+
authority_id: authorityId,
|
|
2894
|
+
tenant_id: tenantId,
|
|
2895
|
+
corpus_id: corpusId
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
function buildMementosProjectRegistrationCapability(options = {}) {
|
|
2899
|
+
const identity = resolveMementosProjectAuthorityIdentity(options);
|
|
2900
|
+
return {
|
|
2901
|
+
authority: "mementos",
|
|
2902
|
+
route: MEMENTOS_PROJECT_REGISTRATION_ROUTE,
|
|
2903
|
+
package_version: options.packageVersion ?? getMementosPackageVersion(),
|
|
2904
|
+
...identity,
|
|
2905
|
+
supported_resources: ["project"],
|
|
2906
|
+
conditional_create: true,
|
|
2907
|
+
immutable_receipts: true,
|
|
2908
|
+
exact_terminal_lookup: true,
|
|
2909
|
+
exact_readback: true,
|
|
2910
|
+
conditional_inverse: true,
|
|
2911
|
+
ambiguous_outcome_reconciliation: true,
|
|
2912
|
+
guarded_update: true,
|
|
2913
|
+
guarded_update_route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
2914
|
+
no_write_dry_run: true,
|
|
2915
|
+
expected_revision_compare_and_swap: true,
|
|
2916
|
+
caller_idempotency: true,
|
|
2917
|
+
exact_inverse_rollback: true,
|
|
2918
|
+
project_resource_enumeration: true,
|
|
2919
|
+
project_resource_route: MEMENTOS_PROJECT_RESOURCE_ROUTE,
|
|
2920
|
+
project_resource_kinds: ["project", "knowledge", "memory", "session"],
|
|
2921
|
+
stable_keyset_pagination: true,
|
|
2922
|
+
explicit_membership_only: true
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2852
2926
|
// src/db/projects.ts
|
|
2853
2927
|
function parseProjectRow(row) {
|
|
2854
2928
|
return {
|
|
@@ -2883,12 +2957,17 @@ class ProjectGuardedUpdateError extends Error {
|
|
|
2883
2957
|
this.name = "ProjectGuardedUpdateError";
|
|
2884
2958
|
}
|
|
2885
2959
|
}
|
|
2886
|
-
var PROJECT_UPDATE_AUTHORITY = {
|
|
2887
|
-
authority_id: "mementos",
|
|
2888
|
-
tenant_id: "default",
|
|
2889
|
-
corpus_id: "default"
|
|
2890
|
-
};
|
|
2891
2960
|
var BOUNDED_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
2961
|
+
function projectUpdateAuthority() {
|
|
2962
|
+
try {
|
|
2963
|
+
return resolveMementosProjectAuthorityIdentity();
|
|
2964
|
+
} catch (error) {
|
|
2965
|
+
if (error instanceof MementosProjectAuthorityIdentityError) {
|
|
2966
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_AUTHORITY_MISMATCH", error.message, { authority_code: error.code, missing_env: error.missing_env });
|
|
2967
|
+
}
|
|
2968
|
+
throw error;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2892
2971
|
function canonicalizeProjectUpdateValue(value) {
|
|
2893
2972
|
if (Array.isArray(value))
|
|
2894
2973
|
return value.map(canonicalizeProjectUpdateValue);
|
|
@@ -2961,7 +3040,7 @@ function normalizeProjectUpdateInput(input) {
|
|
|
2961
3040
|
}
|
|
2962
3041
|
return normalized;
|
|
2963
3042
|
}
|
|
2964
|
-
function assertProjectUpdateIdentity(identity, expectedIdentity =
|
|
3043
|
+
function assertProjectUpdateIdentity(identity, expectedIdentity = projectUpdateAuthority()) {
|
|
2965
3044
|
if (identity.authority_id !== expectedIdentity.authority_id || identity.tenant_id !== expectedIdentity.tenant_id || identity.corpus_id !== expectedIdentity.corpus_id) {
|
|
2966
3045
|
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_AUTHORITY_MISMATCH", "guarded project update does not match this authority, tenant, and corpus");
|
|
2967
3046
|
}
|
|
@@ -2971,7 +3050,7 @@ function assertBoundedIdentifier(value, field) {
|
|
|
2971
3050
|
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", `${field} must be an 8-128 character bounded identifier`);
|
|
2972
3051
|
}
|
|
2973
3052
|
}
|
|
2974
|
-
function assertProjectUpdateRequest(request, expectedIdentity =
|
|
3053
|
+
function assertProjectUpdateRequest(request, expectedIdentity = projectUpdateAuthority()) {
|
|
2975
3054
|
assertProjectUpdateIdentity(request, expectedIdentity);
|
|
2976
3055
|
assertBoundedIdentifier(request.operation_id, "operation_id");
|
|
2977
3056
|
assertBoundedIdentifier(request.step_id, "step_id");
|
|
@@ -3149,7 +3228,7 @@ function listProjects(db) {
|
|
|
3149
3228
|
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
3150
3229
|
return rows.map(parseProjectRow);
|
|
3151
3230
|
}
|
|
3152
|
-
function previewProjectUpdate(id, request, db, expectedIdentity =
|
|
3231
|
+
function previewProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority()) {
|
|
3153
3232
|
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3154
3233
|
const normalized = normalizeProjectUpdateInput(request.updates);
|
|
3155
3234
|
if (!db && isApiMode()) {
|
|
@@ -3172,7 +3251,7 @@ function previewProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDATE
|
|
|
3172
3251
|
receipt: null
|
|
3173
3252
|
};
|
|
3174
3253
|
}
|
|
3175
|
-
function applyProjectUpdate(id, request, db, expectedIdentity =
|
|
3254
|
+
function applyProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority(), resultDigestForProject) {
|
|
3176
3255
|
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3177
3256
|
const normalized = normalizeProjectUpdateInput(request.updates);
|
|
3178
3257
|
if (!db && isApiMode()) {
|
|
@@ -3241,7 +3320,7 @@ function applyProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDATE_A
|
|
|
3241
3320
|
return { dry_run: false, applied: true, project: readback, receipt };
|
|
3242
3321
|
});
|
|
3243
3322
|
}
|
|
3244
|
-
function rollbackProjectUpdate(id, request, db, expectedIdentity =
|
|
3323
|
+
function rollbackProjectUpdate(id, request, db, expectedIdentity = projectUpdateAuthority(), resultDigestForProject) {
|
|
3245
3324
|
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3246
3325
|
assertBoundedIdentifier(request.accepted_receipt_id, "accepted_receipt_id");
|
|
3247
3326
|
if (!db && isApiMode()) {
|
|
@@ -3315,7 +3394,7 @@ function rollbackProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDAT
|
|
|
3315
3394
|
return { dry_run: false, applied: true, project: readback, receipt };
|
|
3316
3395
|
});
|
|
3317
3396
|
}
|
|
3318
|
-
function getProjectUpdateReceipt(id, receiptId, identity =
|
|
3397
|
+
function getProjectUpdateReceipt(id, receiptId, identity = projectUpdateAuthority(), db, expectedIdentity = projectUpdateAuthority()) {
|
|
3319
3398
|
assertProjectUpdateIdentity(identity, expectedIdentity);
|
|
3320
3399
|
if (!db && isApiMode()) {
|
|
3321
3400
|
const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/update-receipts/lookup`, { ...identity, receipt_id: receiptId });
|
|
@@ -3954,27 +4033,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
3954
4033
|
this.db = db;
|
|
3955
4034
|
this.now = options.now ?? (() => new Date().toISOString());
|
|
3956
4035
|
this.faultInjector = options.faultInjector;
|
|
3957
|
-
this.capabilityValue =
|
|
3958
|
-
authority: "mementos",
|
|
3959
|
-
route: MEMENTOS_PROJECT_REGISTRATION_ROUTE,
|
|
3960
|
-
package_version: options.packageVersion ?? getMementosPackageVersion(),
|
|
3961
|
-
authority_id: options.authorityId ?? "mementos",
|
|
3962
|
-
tenant_id: options.tenantId ?? "default",
|
|
3963
|
-
corpus_id: options.corpusId ?? "default",
|
|
3964
|
-
supported_resources: ["project"],
|
|
3965
|
-
conditional_create: true,
|
|
3966
|
-
immutable_receipts: true,
|
|
3967
|
-
exact_terminal_lookup: true,
|
|
3968
|
-
exact_readback: true,
|
|
3969
|
-
conditional_inverse: true,
|
|
3970
|
-
ambiguous_outcome_reconciliation: true,
|
|
3971
|
-
guarded_update: true,
|
|
3972
|
-
guarded_update_route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
3973
|
-
no_write_dry_run: true,
|
|
3974
|
-
expected_revision_compare_and_swap: true,
|
|
3975
|
-
caller_idempotency: true,
|
|
3976
|
-
exact_inverse_rollback: true
|
|
3977
|
-
};
|
|
4036
|
+
this.capabilityValue = buildMementosProjectRegistrationCapability(options);
|
|
3978
4037
|
}
|
|
3979
4038
|
fault(point, request) {
|
|
3980
4039
|
this.faultInjector?.(point, {
|
|
@@ -4670,26 +4729,1400 @@ class MementosProjectRegistrationHttpClient {
|
|
|
4670
4729
|
function createMementosProjectRegistrationHttpClient(options) {
|
|
4671
4730
|
return new MementosProjectRegistrationHttpClient(options);
|
|
4672
4731
|
}
|
|
4732
|
+
// src/project-registration/project-resources.ts
|
|
4733
|
+
init_api_mode();
|
|
4734
|
+
init_database();
|
|
4735
|
+
|
|
4736
|
+
// src/types/index.ts
|
|
4737
|
+
class AgentConflictError extends Error {
|
|
4738
|
+
conflict = true;
|
|
4739
|
+
existing_id;
|
|
4740
|
+
existing_name;
|
|
4741
|
+
last_seen_at;
|
|
4742
|
+
session_hint;
|
|
4743
|
+
working_dir;
|
|
4744
|
+
constructor(opts) {
|
|
4745
|
+
const msg = `Agent "${opts.existing_name}" is already active (session hint: ${opts.session_hint ?? "unknown"}, last seen ${opts.last_seen_at}). Wait 30 minutes or use a different name.`;
|
|
4746
|
+
super(msg);
|
|
4747
|
+
this.name = "AgentConflictError";
|
|
4748
|
+
this.existing_id = opts.existing_id;
|
|
4749
|
+
this.existing_name = opts.existing_name;
|
|
4750
|
+
this.last_seen_at = opts.last_seen_at;
|
|
4751
|
+
this.session_hint = opts.session_hint;
|
|
4752
|
+
this.working_dir = opts.working_dir ?? null;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
class EntityNotFoundError extends Error {
|
|
4756
|
+
constructor(id) {
|
|
4757
|
+
super(`Entity not found: ${id}`);
|
|
4758
|
+
this.name = "EntityNotFoundError";
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
class MemoryNotFoundError extends Error {
|
|
4763
|
+
constructor(id) {
|
|
4764
|
+
super(`Memory not found: ${id}`);
|
|
4765
|
+
this.name = "MemoryNotFoundError";
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
class DuplicateMemoryError extends Error {
|
|
4770
|
+
constructor(key, scope) {
|
|
4771
|
+
super(`Memory already exists with key "${key}" in scope "${scope}"`);
|
|
4772
|
+
this.name = "DuplicateMemoryError";
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
class MemoryExpiredError extends Error {
|
|
4777
|
+
constructor(id) {
|
|
4778
|
+
super(`Memory has expired: ${id}`);
|
|
4779
|
+
this.name = "MemoryExpiredError";
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4783
|
+
class InvalidScopeError extends Error {
|
|
4784
|
+
constructor(message) {
|
|
4785
|
+
super(message);
|
|
4786
|
+
this.name = "InvalidScopeError";
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
class VersionConflictError extends Error {
|
|
4791
|
+
expected;
|
|
4792
|
+
actual;
|
|
4793
|
+
constructor(id, expected, actual) {
|
|
4794
|
+
super(`Version conflict for memory ${id}: expected ${expected}, got ${actual}`);
|
|
4795
|
+
this.name = "VersionConflictError";
|
|
4796
|
+
this.expected = expected;
|
|
4797
|
+
this.actual = actual;
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
class MemoryConflictError extends Error {
|
|
4802
|
+
existingId;
|
|
4803
|
+
existingAgentId;
|
|
4804
|
+
existingUpdatedAt;
|
|
4805
|
+
constructor(key, existing) {
|
|
4806
|
+
super(`Memory conflict: key "${key}" already exists (last written by ${existing.agent_id ?? "unknown"} at ${existing.updated_at}). Use conflict:"overwrite" to replace it.`);
|
|
4807
|
+
this.name = "MemoryConflictError";
|
|
4808
|
+
this.existingId = existing.id;
|
|
4809
|
+
this.existingAgentId = existing.agent_id;
|
|
4810
|
+
this.existingUpdatedAt = existing.updated_at;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
// src/db/memories.ts
|
|
4815
|
+
init_database();
|
|
4816
|
+
|
|
4817
|
+
// src/lib/embeddings.ts
|
|
4818
|
+
function cosineSimilarity(a, b) {
|
|
4819
|
+
if (a.length !== b.length || a.length === 0)
|
|
4820
|
+
return 0;
|
|
4821
|
+
let dot = 0, magA = 0, magB = 0;
|
|
4822
|
+
for (let i = 0;i < a.length; i++) {
|
|
4823
|
+
dot += a[i] * b[i];
|
|
4824
|
+
magA += a[i] * a[i];
|
|
4825
|
+
magB += b[i] * b[i];
|
|
4826
|
+
}
|
|
4827
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
4828
|
+
return denom === 0 ? 0 : dot / denom;
|
|
4829
|
+
}
|
|
4830
|
+
var OPENAI_EMBED_URL = "https://api.openai.com/v1/embeddings";
|
|
4831
|
+
var EMBED_MODEL = "text-embedding-3-small";
|
|
4832
|
+
var EMBED_DIMENSIONS = 1536;
|
|
4833
|
+
async function openAIEmbed(text, apiKey) {
|
|
4834
|
+
const res = await fetch(OPENAI_EMBED_URL, {
|
|
4835
|
+
method: "POST",
|
|
4836
|
+
headers: {
|
|
4837
|
+
"Content-Type": "application/json",
|
|
4838
|
+
Authorization: `Bearer ${apiKey}`
|
|
4839
|
+
},
|
|
4840
|
+
body: JSON.stringify({
|
|
4841
|
+
model: EMBED_MODEL,
|
|
4842
|
+
input: text.slice(0, 8192)
|
|
4843
|
+
}),
|
|
4844
|
+
signal: AbortSignal.timeout(1e4)
|
|
4845
|
+
});
|
|
4846
|
+
if (!res.ok) {
|
|
4847
|
+
throw new Error(`OpenAI embedding API ${res.status}: ${await res.text()}`);
|
|
4848
|
+
}
|
|
4849
|
+
const data = await res.json();
|
|
4850
|
+
return data.data[0].embedding;
|
|
4851
|
+
}
|
|
4852
|
+
function tfidfVector(text) {
|
|
4853
|
+
const DIMS = 512;
|
|
4854
|
+
const vec = new Float32Array(DIMS);
|
|
4855
|
+
const tokens = text.toLowerCase().match(/\b\w+\b/g) ?? [];
|
|
4856
|
+
for (const token of tokens) {
|
|
4857
|
+
let hash = 2166136261;
|
|
4858
|
+
for (let i = 0;i < token.length; i++) {
|
|
4859
|
+
hash ^= token.charCodeAt(i);
|
|
4860
|
+
hash = hash * 16777619 >>> 0;
|
|
4861
|
+
}
|
|
4862
|
+
vec[hash % DIMS] += 1;
|
|
4863
|
+
}
|
|
4864
|
+
let norm = 0;
|
|
4865
|
+
for (let i = 0;i < DIMS; i++)
|
|
4866
|
+
norm += vec[i] * vec[i];
|
|
4867
|
+
norm = Math.sqrt(norm);
|
|
4868
|
+
if (norm > 0)
|
|
4869
|
+
for (let i = 0;i < DIMS; i++)
|
|
4870
|
+
vec[i] /= norm;
|
|
4871
|
+
return Array.from(vec);
|
|
4872
|
+
}
|
|
4873
|
+
async function generateEmbedding(text) {
|
|
4874
|
+
const apiKey = process.env["OPENAI_API_KEY"];
|
|
4875
|
+
if (apiKey) {
|
|
4876
|
+
try {
|
|
4877
|
+
const embedding2 = await openAIEmbed(text, apiKey);
|
|
4878
|
+
return { embedding: embedding2, model: EMBED_MODEL, dimensions: EMBED_DIMENSIONS };
|
|
4879
|
+
} catch {}
|
|
4880
|
+
}
|
|
4881
|
+
const embedding = tfidfVector(text);
|
|
4882
|
+
return { embedding, model: "tfidf-512", dimensions: 512 };
|
|
4883
|
+
}
|
|
4884
|
+
function deserializeEmbedding(raw) {
|
|
4885
|
+
return JSON.parse(raw);
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
// src/lib/redact.ts
|
|
4889
|
+
var REDACTED = "[REDACTED]";
|
|
4890
|
+
var SECRET_PATTERNS = [
|
|
4891
|
+
{ name: "openai_key", pattern: /sk-[a-zA-Z0-9_-]{20,}/g },
|
|
4892
|
+
{ name: "anthropic_key", pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g },
|
|
4893
|
+
{ name: "generic_key", pattern: /(?:pk|tok|key|token|api[_-]?key)[_-][a-zA-Z0-9_-]{16,}/gi },
|
|
4894
|
+
{ name: "aws_key", pattern: /AKIA[A-Z0-9]{16}/g },
|
|
4895
|
+
{ name: "aws_secret", pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g },
|
|
4896
|
+
{ name: "github_token", pattern: /gh[ps]_[a-zA-Z0-9]{36,}/g },
|
|
4897
|
+
{ name: "github_oauth", pattern: /gho_[a-zA-Z0-9]{36,}/g },
|
|
4898
|
+
{ name: "npm_token", pattern: /npm_[a-zA-Z0-9]{36,}/g },
|
|
4899
|
+
{ name: "bearer", pattern: /Bearer\s+[a-zA-Z0-9_\-.]{20,}/g },
|
|
4900
|
+
{ name: "conn_string", pattern: /(?:postgres|postgresql|mysql|mongodb|redis|amqp|mqtt):\/\/[^\s"'`]+@[^\s"'`]+/gi },
|
|
4901
|
+
{ name: "env_secret", pattern: /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|API_KEY|PRIVATE_KEY|AUTH|CREDENTIAL)[_A-Z]*\s*=\s*["']?[^\s"'\n]{8,}["']?/gi },
|
|
4902
|
+
{ name: "stripe_key", pattern: /(?:sk|pk|rk)_(?:test|live)_[a-zA-Z0-9]{20,}/g },
|
|
4903
|
+
{ name: "slack_token", pattern: /xox[bpras]-[a-zA-Z0-9-]{20,}/g },
|
|
4904
|
+
{ name: "jwt", pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
4905
|
+
{ name: "hex_secret", pattern: /(?<=(?:key|token|secret|password|hash)\s*[:=]\s*["']?)[0-9a-f]{32,}(?=["']?)/gi }
|
|
4906
|
+
];
|
|
4907
|
+
function redactSecrets(text) {
|
|
4908
|
+
let result = text;
|
|
4909
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
4910
|
+
pattern.lastIndex = 0;
|
|
4911
|
+
result = result.replace(pattern, REDACTED);
|
|
4912
|
+
}
|
|
4913
|
+
return result;
|
|
4914
|
+
}
|
|
4915
|
+
function containsSecrets(text) {
|
|
4916
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
4917
|
+
pattern.lastIndex = 0;
|
|
4918
|
+
if (pattern.test(text))
|
|
4919
|
+
return true;
|
|
4920
|
+
}
|
|
4921
|
+
return false;
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
// src/lib/hooks.ts
|
|
4925
|
+
var _idCounter = 0;
|
|
4926
|
+
function generateHookId() {
|
|
4927
|
+
return `hook_${++_idCounter}_${Date.now().toString(36)}`;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
class HookRegistry {
|
|
4931
|
+
hooks = new Map;
|
|
4932
|
+
register(reg) {
|
|
4933
|
+
const id = generateHookId();
|
|
4934
|
+
const hook = {
|
|
4935
|
+
...reg,
|
|
4936
|
+
id,
|
|
4937
|
+
priority: reg.priority ?? 50
|
|
4938
|
+
};
|
|
4939
|
+
this.hooks.set(id, hook);
|
|
4940
|
+
return id;
|
|
4941
|
+
}
|
|
4942
|
+
unregister(hookId) {
|
|
4943
|
+
const hook = this.hooks.get(hookId);
|
|
4944
|
+
if (!hook)
|
|
4945
|
+
return false;
|
|
4946
|
+
if (hook.builtin)
|
|
4947
|
+
return false;
|
|
4948
|
+
this.hooks.delete(hookId);
|
|
4949
|
+
return true;
|
|
4950
|
+
}
|
|
4951
|
+
list(type) {
|
|
4952
|
+
const all = [...this.hooks.values()];
|
|
4953
|
+
if (!type)
|
|
4954
|
+
return all;
|
|
4955
|
+
return all.filter((h) => h.type === type);
|
|
4956
|
+
}
|
|
4957
|
+
async runHooks(type, context) {
|
|
4958
|
+
const matching = this.getMatchingHooks(type, context);
|
|
4959
|
+
if (matching.length === 0)
|
|
4960
|
+
return true;
|
|
4961
|
+
matching.sort((a, b) => a.priority - b.priority);
|
|
4962
|
+
for (const hook of matching) {
|
|
4963
|
+
if (hook.blocking) {
|
|
4964
|
+
try {
|
|
4965
|
+
const result = await hook.handler(context);
|
|
4966
|
+
if (result === false)
|
|
4967
|
+
return false;
|
|
4968
|
+
} catch (err) {
|
|
4969
|
+
console.error(`[hooks] blocking hook ${hook.id} (${type}) threw:`, err);
|
|
4970
|
+
}
|
|
4971
|
+
} else {
|
|
4972
|
+
Promise.resolve().then(() => hook.handler(context)).catch((err) => console.error(`[hooks] non-blocking hook ${hook.id} (${type}) threw:`, err));
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
return true;
|
|
4976
|
+
}
|
|
4977
|
+
getMatchingHooks(type, context) {
|
|
4978
|
+
const ctx = context;
|
|
4979
|
+
return [...this.hooks.values()].filter((hook) => {
|
|
4980
|
+
if (hook.type !== type)
|
|
4981
|
+
return false;
|
|
4982
|
+
if (hook.agentId && hook.agentId !== ctx.agentId)
|
|
4983
|
+
return false;
|
|
4984
|
+
if (hook.projectId && hook.projectId !== ctx.projectId)
|
|
4985
|
+
return false;
|
|
4986
|
+
return true;
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
stats() {
|
|
4990
|
+
const all = [...this.hooks.values()];
|
|
4991
|
+
const byType = {};
|
|
4992
|
+
for (const hook of all) {
|
|
4993
|
+
byType[hook.type] = (byType[hook.type] ?? 0) + 1;
|
|
4994
|
+
}
|
|
4995
|
+
return {
|
|
4996
|
+
total: all.length,
|
|
4997
|
+
byType,
|
|
4998
|
+
blocking: all.filter((h) => h.blocking).length,
|
|
4999
|
+
nonBlocking: all.filter((h) => !h.blocking).length
|
|
5000
|
+
};
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
var hookRegistry = new HookRegistry;
|
|
5004
|
+
|
|
5005
|
+
// src/lib/poisoning.ts
|
|
5006
|
+
var INSTRUCTION_PATTERNS = [
|
|
5007
|
+
/\byou\s+must\b/i,
|
|
5008
|
+
/\byou\s+should\b/i,
|
|
5009
|
+
/\byou\s+have\s+to\b/i,
|
|
5010
|
+
/\balways\b/i,
|
|
5011
|
+
/\bnever\b/i
|
|
5012
|
+
];
|
|
5013
|
+
var PROMOTIONAL_PATTERNS = [
|
|
5014
|
+
/\brecommend\b/i,
|
|
5015
|
+
/\bbuy\b/i,
|
|
5016
|
+
/\bbest\s+product\b/i,
|
|
5017
|
+
/\bpurchase\b/i,
|
|
5018
|
+
/\bdiscount\b/i,
|
|
5019
|
+
/\baffiliate\b/i
|
|
5020
|
+
];
|
|
5021
|
+
function computeTrustScore(value, key, existingMemories, claimedImportance) {
|
|
5022
|
+
let score = 1;
|
|
5023
|
+
for (const pattern of INSTRUCTION_PATTERNS) {
|
|
5024
|
+
if (pattern.test(value)) {
|
|
5025
|
+
score -= 0.2;
|
|
5026
|
+
break;
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
for (const pattern of PROMOTIONAL_PATTERNS) {
|
|
5030
|
+
if (pattern.test(value)) {
|
|
5031
|
+
score -= 0.3;
|
|
5032
|
+
break;
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
if (existingMemories && existingMemories.length > 0) {
|
|
5036
|
+
for (const existing of existingMemories) {
|
|
5037
|
+
if (existing.key !== key)
|
|
5038
|
+
continue;
|
|
5039
|
+
if (existing.importance < 7)
|
|
5040
|
+
continue;
|
|
5041
|
+
if (existing.status !== "active")
|
|
5042
|
+
continue;
|
|
5043
|
+
const existingLower = existing.value.toLowerCase().trim();
|
|
5044
|
+
const newLower = value.toLowerCase().trim();
|
|
5045
|
+
if (existingLower === newLower)
|
|
5046
|
+
continue;
|
|
5047
|
+
const existingWords = new Set(existingLower.split(/\s+/));
|
|
5048
|
+
const newWords = new Set(newLower.split(/\s+/));
|
|
5049
|
+
let overlap = 0;
|
|
5050
|
+
for (const w of newWords) {
|
|
5051
|
+
if (existingWords.has(w))
|
|
5052
|
+
overlap++;
|
|
5053
|
+
}
|
|
5054
|
+
const totalUnique = new Set([...newWords, ...existingWords]).size;
|
|
5055
|
+
const overlapRatio = totalUnique > 0 ? overlap / totalUnique : 0;
|
|
5056
|
+
if (overlapRatio < 0.3) {
|
|
5057
|
+
score -= 0.3;
|
|
5058
|
+
break;
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
if (value.length < 10 && (claimedImportance ?? 5) >= 8) {
|
|
5063
|
+
score -= 0.1;
|
|
5064
|
+
}
|
|
5065
|
+
return Math.max(0, Math.min(1, score));
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
// src/db/entity-memories.ts
|
|
5069
|
+
init_database();
|
|
5070
|
+
init_api_mode();
|
|
5071
|
+
function parseEntityRow(row) {
|
|
5072
|
+
return {
|
|
5073
|
+
id: row["id"],
|
|
5074
|
+
name: row["name"],
|
|
5075
|
+
type: row["type"],
|
|
5076
|
+
description: row["description"] || null,
|
|
5077
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5078
|
+
project_id: row["project_id"] || null,
|
|
5079
|
+
created_at: row["created_at"],
|
|
5080
|
+
updated_at: row["updated_at"]
|
|
5081
|
+
};
|
|
5082
|
+
}
|
|
5083
|
+
function parseEntityMemoryRow(row) {
|
|
5084
|
+
return {
|
|
5085
|
+
entity_id: row["entity_id"],
|
|
5086
|
+
memory_id: row["memory_id"],
|
|
5087
|
+
role: row["role"],
|
|
5088
|
+
created_at: row["created_at"]
|
|
5089
|
+
};
|
|
5090
|
+
}
|
|
5091
|
+
function linkEntityToMemory(entityId, memoryId, role = "context", db) {
|
|
5092
|
+
if (!db && isApiMode()) {
|
|
5093
|
+
const { data } = apiJson("POST", `/entities/${encodeURIComponent(entityId)}/memories`, {
|
|
5094
|
+
memory_id: memoryId,
|
|
5095
|
+
role
|
|
5096
|
+
});
|
|
5097
|
+
return data;
|
|
5098
|
+
}
|
|
5099
|
+
const d = db || getDatabase();
|
|
5100
|
+
const timestamp = now();
|
|
5101
|
+
d.run(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
5102
|
+
VALUES (?, ?, ?, ?)`, [entityId, memoryId, role, timestamp]);
|
|
5103
|
+
const row = d.query("SELECT * FROM entity_memories WHERE entity_id = ? AND memory_id = ?").get(entityId, memoryId);
|
|
5104
|
+
return parseEntityMemoryRow(row);
|
|
5105
|
+
}
|
|
5106
|
+
function unlinkEntityFromMemory(entityId, memoryId, db) {
|
|
5107
|
+
if (!db && isApiMode()) {
|
|
5108
|
+
apiJson("DELETE", `/entities/${encodeURIComponent(entityId)}/memories/${encodeURIComponent(memoryId)}`);
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
const d = db || getDatabase();
|
|
5112
|
+
d.run("DELETE FROM entity_memories WHERE entity_id = ? AND memory_id = ?", [entityId, memoryId]);
|
|
5113
|
+
}
|
|
5114
|
+
function getMemoriesForEntity(entityId, db) {
|
|
5115
|
+
if (!db && isApiMode()) {
|
|
5116
|
+
const { data } = apiJson("GET", `/entities/${encodeURIComponent(entityId)}/memories`);
|
|
5117
|
+
return data?.memories ?? [];
|
|
5118
|
+
}
|
|
5119
|
+
const d = db || getDatabase();
|
|
5120
|
+
const rows = d.query(`SELECT m.* FROM memories m
|
|
5121
|
+
INNER JOIN entity_memories em ON em.memory_id = m.id
|
|
5122
|
+
WHERE em.entity_id = ?
|
|
5123
|
+
ORDER BY m.importance DESC, m.created_at DESC`).all(entityId);
|
|
5124
|
+
return rows.map(parseMemoryRow);
|
|
5125
|
+
}
|
|
5126
|
+
function getEntitiesForMemory(memoryId, db) {
|
|
5127
|
+
const d = db || getDatabase();
|
|
5128
|
+
const rows = d.query(`SELECT e.* FROM entities e
|
|
5129
|
+
INNER JOIN entity_memories em ON em.entity_id = e.id
|
|
5130
|
+
WHERE em.memory_id = ?
|
|
5131
|
+
ORDER BY e.name ASC`).all(memoryId);
|
|
5132
|
+
return rows.map(parseEntityRow);
|
|
5133
|
+
}
|
|
5134
|
+
function bulkLinkEntities(entityIds, memoryId, role = "context", db) {
|
|
5135
|
+
const d = db || getDatabase();
|
|
5136
|
+
const timestamp = now();
|
|
5137
|
+
d.transaction(() => {
|
|
5138
|
+
const stmt = d.prepare(`INSERT OR IGNORE INTO entity_memories (entity_id, memory_id, role, created_at)
|
|
5139
|
+
VALUES (?, ?, ?, ?)`);
|
|
5140
|
+
for (const entityId of entityIds) {
|
|
5141
|
+
stmt.run(entityId, memoryId, role, timestamp);
|
|
5142
|
+
}
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
function getEntityMemoryLinks(entityId, memoryId, db) {
|
|
5146
|
+
const d = db || getDatabase();
|
|
5147
|
+
const conditions = [];
|
|
5148
|
+
const params = [];
|
|
5149
|
+
if (entityId) {
|
|
5150
|
+
conditions.push("entity_id = ?");
|
|
5151
|
+
params.push(entityId);
|
|
5152
|
+
}
|
|
5153
|
+
if (memoryId) {
|
|
5154
|
+
conditions.push("memory_id = ?");
|
|
5155
|
+
params.push(memoryId);
|
|
5156
|
+
}
|
|
5157
|
+
let sql = "SELECT * FROM entity_memories";
|
|
5158
|
+
if (conditions.length > 0) {
|
|
5159
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
5160
|
+
}
|
|
5161
|
+
sql += " ORDER BY created_at DESC";
|
|
5162
|
+
const rows = d.query(sql).all(...params);
|
|
5163
|
+
return rows.map(parseEntityMemoryRow);
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
// src/db/memories.ts
|
|
5167
|
+
init_api_mode();
|
|
5168
|
+
function runEntityExtraction(_memory, _projectId, _d) {}
|
|
5169
|
+
function applyContentType(d, id, memory, contentType) {
|
|
5170
|
+
if (!contentType || contentType === "text")
|
|
5171
|
+
return;
|
|
5172
|
+
try {
|
|
5173
|
+
d.run("UPDATE memories SET content_type = ? WHERE id = ?", [contentType, id]);
|
|
5174
|
+
memory.content_type = contentType;
|
|
5175
|
+
} catch {}
|
|
5176
|
+
}
|
|
5177
|
+
function parseMemoryRow(row) {
|
|
5178
|
+
return {
|
|
5179
|
+
id: row["id"],
|
|
5180
|
+
key: row["key"],
|
|
5181
|
+
value: row["value"],
|
|
5182
|
+
category: row["category"],
|
|
5183
|
+
scope: row["scope"],
|
|
5184
|
+
summary: row["summary"] || null,
|
|
5185
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
5186
|
+
importance: row["importance"],
|
|
5187
|
+
source: row["source"],
|
|
5188
|
+
status: row["status"],
|
|
5189
|
+
pinned: !!row["pinned"],
|
|
5190
|
+
agent_id: row["agent_id"] || null,
|
|
5191
|
+
project_id: row["project_id"] || null,
|
|
5192
|
+
session_id: row["session_id"] || null,
|
|
5193
|
+
machine_id: row["machine_id"] || null,
|
|
5194
|
+
flag: row["flag"] || null,
|
|
5195
|
+
when_to_use: row["when_to_use"] || null,
|
|
5196
|
+
sequence_group: row["sequence_group"] || null,
|
|
5197
|
+
sequence_order: row["sequence_order"] ?? null,
|
|
5198
|
+
content_type: row["content_type"] || "text",
|
|
5199
|
+
namespace: row["namespace"] || null,
|
|
5200
|
+
created_by_agent: row["created_by_agent"] || null,
|
|
5201
|
+
updated_by_agent: row["updated_by_agent"] || null,
|
|
5202
|
+
trust_score: row["trust_score"] != null ? row["trust_score"] : null,
|
|
5203
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
5204
|
+
access_count: row["access_count"],
|
|
5205
|
+
version: row["version"],
|
|
5206
|
+
expires_at: row["expires_at"] || null,
|
|
5207
|
+
valid_from: row["valid_from"] || null,
|
|
5208
|
+
valid_until: row["valid_until"] || null,
|
|
5209
|
+
ingested_at: row["ingested_at"] || null,
|
|
5210
|
+
created_at: row["created_at"],
|
|
5211
|
+
updated_at: row["updated_at"],
|
|
5212
|
+
accessed_at: row["accessed_at"] || null
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5215
|
+
function createMemory(input, dedupeMode = "merge", db) {
|
|
5216
|
+
if (!db && isApiMode()) {
|
|
5217
|
+
const { status, data } = apiJson("POST", "/memories", { ...input, dedupe: dedupeMode });
|
|
5218
|
+
if (!data || !data.id) {
|
|
5219
|
+
throw new ApiRequestError(`mementos cloud POST /memories \u2192 ${status} but no memory was returned; the write did not persist (key: ${input.key})`, status, "");
|
|
5220
|
+
}
|
|
5221
|
+
return data;
|
|
5222
|
+
}
|
|
5223
|
+
const d = db || getDatabase();
|
|
5224
|
+
const timestamp = now();
|
|
5225
|
+
if (input.project_id) {
|
|
5226
|
+
const resolved = resolvePartialId(d, "projects", input.project_id);
|
|
5227
|
+
if (resolved) {
|
|
5228
|
+
input = { ...input, project_id: resolved };
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
let expiresAt = input.expires_at || null;
|
|
5232
|
+
if (input.ttl_ms && !expiresAt) {
|
|
5233
|
+
expiresAt = new Date(Date.now() + input.ttl_ms).toISOString();
|
|
5234
|
+
}
|
|
5235
|
+
if (input.scope === "working" && !expiresAt) {
|
|
5236
|
+
expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
5237
|
+
}
|
|
5238
|
+
const id = uuid();
|
|
5239
|
+
const tags = input.tags || [];
|
|
5240
|
+
const tagsJson = JSON.stringify(tags);
|
|
5241
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
5242
|
+
const safeValue = redactSecrets(input.value);
|
|
5243
|
+
const safeSummary = input.summary ? redactSecrets(input.summary) : null;
|
|
5244
|
+
const effectiveMode = dedupeMode === "overwrite" ? "merge" : dedupeMode === "version-fork" ? "create" : dedupeMode;
|
|
5245
|
+
if (effectiveMode === "error") {
|
|
5246
|
+
const existing = d.query(`SELECT id, agent_id, updated_at FROM memories
|
|
5247
|
+
WHERE key = ? AND scope = ? AND COALESCE(project_id, '') = ? AND status = 'active'
|
|
5248
|
+
LIMIT 1`).get(input.key, input.scope || "private", input.project_id || "");
|
|
5249
|
+
if (existing) {
|
|
5250
|
+
throw new MemoryConflictError(input.key, existing);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
if (effectiveMode === "merge") {
|
|
5254
|
+
const existing = d.query(`SELECT id, version FROM memories
|
|
5255
|
+
WHERE key = ? AND scope = ?
|
|
5256
|
+
AND COALESCE(agent_id, '') = ?
|
|
5257
|
+
AND COALESCE(project_id, '') = ?
|
|
5258
|
+
AND COALESCE(session_id, '') = ?`).get(input.key, input.scope || "private", input.agent_id || "", input.project_id || "", input.session_id || "");
|
|
5259
|
+
if (existing) {
|
|
5260
|
+
d.run(`UPDATE memories SET
|
|
5261
|
+
value = ?, category = ?, summary = ?, tags = ?,
|
|
5262
|
+
importance = ?, metadata = ?, expires_at = ?,
|
|
5263
|
+
when_to_use = ?,
|
|
5264
|
+
pinned = COALESCE(pinned, 0),
|
|
5265
|
+
version = version + 1, updated_at = ?
|
|
5266
|
+
WHERE id = ?`, [
|
|
5267
|
+
safeValue,
|
|
5268
|
+
input.category || "knowledge",
|
|
5269
|
+
safeSummary,
|
|
5270
|
+
tagsJson,
|
|
5271
|
+
input.importance ?? 5,
|
|
5272
|
+
metadataJson,
|
|
5273
|
+
expiresAt,
|
|
5274
|
+
input.when_to_use || null,
|
|
5275
|
+
timestamp,
|
|
5276
|
+
existing.id
|
|
5277
|
+
]);
|
|
5278
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [existing.id]);
|
|
5279
|
+
const insertTag2 = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5280
|
+
for (const tag of tags) {
|
|
5281
|
+
insertTag2.run(existing.id, tag);
|
|
5282
|
+
}
|
|
5283
|
+
const merged = getMemory(existing.id, d);
|
|
5284
|
+
applyContentType(d, existing.id, merged, input.content_type);
|
|
5285
|
+
try {
|
|
5286
|
+
const existingMemories = listMemoriesByKey(input.key, d);
|
|
5287
|
+
const trustScore = computeTrustScore(safeValue, input.key, existingMemories, input.importance);
|
|
5288
|
+
d.run("UPDATE memories SET trust_score = ? WHERE id = ?", [trustScore, existing.id]);
|
|
5289
|
+
} catch {}
|
|
5290
|
+
try {
|
|
5291
|
+
const oldLinks = getEntityMemoryLinks(undefined, merged.id, d);
|
|
5292
|
+
for (const link of oldLinks) {
|
|
5293
|
+
unlinkEntityFromMemory(link.entity_id, merged.id, d);
|
|
5294
|
+
}
|
|
5295
|
+
runEntityExtraction(merged, input.project_id, d);
|
|
5296
|
+
} catch {}
|
|
5297
|
+
return merged;
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
ensureMemoryReferences(d, input);
|
|
5301
|
+
d.run(`INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, namespace, created_by_agent, when_to_use, sequence_group, sequence_order, metadata, access_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at)
|
|
5302
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', FALSE, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?, ?, ?, ?, ?)`, [
|
|
5303
|
+
id,
|
|
5304
|
+
input.key,
|
|
5305
|
+
safeValue,
|
|
5306
|
+
input.category || "knowledge",
|
|
5307
|
+
input.scope || "private",
|
|
5308
|
+
input.summary || null,
|
|
5309
|
+
tagsJson,
|
|
5310
|
+
input.importance ?? 5,
|
|
5311
|
+
input.source || "agent",
|
|
5312
|
+
input.agent_id || null,
|
|
5313
|
+
input.project_id || null,
|
|
5314
|
+
input.session_id || null,
|
|
5315
|
+
input.machine_id || null,
|
|
5316
|
+
input.namespace || null,
|
|
5317
|
+
input.agent_id || null,
|
|
5318
|
+
input.when_to_use || null,
|
|
5319
|
+
input.sequence_group || null,
|
|
5320
|
+
input.sequence_order ?? null,
|
|
5321
|
+
metadataJson,
|
|
5322
|
+
expiresAt,
|
|
5323
|
+
input.metadata?.valid_from ?? timestamp,
|
|
5324
|
+
input.metadata?.valid_until ?? null,
|
|
5325
|
+
timestamp,
|
|
5326
|
+
timestamp,
|
|
5327
|
+
timestamp
|
|
5328
|
+
]);
|
|
5329
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5330
|
+
for (const tag of tags) {
|
|
5331
|
+
insertTag.run(id, tag);
|
|
5332
|
+
}
|
|
5333
|
+
const memory = getMemory(id, d);
|
|
5334
|
+
applyContentType(d, id, memory, input.content_type);
|
|
5335
|
+
try {
|
|
5336
|
+
const existingMemories = listMemoriesByKey(input.key, d);
|
|
5337
|
+
const trustScore = computeTrustScore(safeValue, input.key, existingMemories, input.importance);
|
|
5338
|
+
d.run("UPDATE memories SET trust_score = ? WHERE id = ?", [trustScore, id]);
|
|
5339
|
+
} catch {}
|
|
5340
|
+
runEntityExtraction(memory, input.project_id, d);
|
|
5341
|
+
hookRegistry.runHooks("PostMemorySave", {
|
|
5342
|
+
memory,
|
|
5343
|
+
wasUpdated: false,
|
|
5344
|
+
agentId: input.agent_id,
|
|
5345
|
+
projectId: input.project_id,
|
|
5346
|
+
sessionId: input.session_id,
|
|
5347
|
+
timestamp: Date.now()
|
|
5348
|
+
});
|
|
5349
|
+
return memory;
|
|
5350
|
+
}
|
|
5351
|
+
function ensureMemoryReferences(d, input) {
|
|
5352
|
+
const t = now();
|
|
5353
|
+
const tryRun = (sql, params) => {
|
|
5354
|
+
try {
|
|
5355
|
+
d.run(sql, params);
|
|
5356
|
+
} catch {}
|
|
5357
|
+
};
|
|
5358
|
+
if (input.agent_id) {
|
|
5359
|
+
tryRun("INSERT OR IGNORE INTO agents (id, name, role, created_at, last_seen_at) VALUES (?, ?, 'agent', ?, ?)", [input.agent_id, `imported-${input.agent_id}`, t, t]);
|
|
5360
|
+
}
|
|
5361
|
+
if (input.project_id) {
|
|
5362
|
+
tryRun("INSERT OR IGNORE INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", [input.project_id, `imported-${input.project_id}`, `imported://${input.project_id}`, t, t]);
|
|
5363
|
+
}
|
|
5364
|
+
if (input.machine_id) {
|
|
5365
|
+
tryRun("INSERT OR IGNORE INTO machines (id, name, hostname, platform, created_at, last_seen_at) VALUES (?, ?, ?, 'unknown', ?, ?)", [input.machine_id, `imported-${input.machine_id}`, `imported-${input.machine_id}`, t, t]);
|
|
5366
|
+
}
|
|
5367
|
+
if (input.session_id) {
|
|
5368
|
+
tryRun("INSERT OR IGNORE INTO sessions (id, agent_id, project_id, started_at, last_activity) VALUES (?, ?, ?, ?, ?)", [input.session_id, input.agent_id ?? null, input.project_id ?? null, t, t]);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
function listMemoriesByKey(key, db) {
|
|
5372
|
+
const rows = db.query("SELECT * FROM memories WHERE key = ? AND status = 'active' ORDER BY importance DESC LIMIT 10").all(key);
|
|
5373
|
+
return rows.map(parseMemoryRow);
|
|
5374
|
+
}
|
|
5375
|
+
function getMemory(id, db) {
|
|
5376
|
+
if (!db && isApiMode()) {
|
|
5377
|
+
const { status, data } = apiJson("GET", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
5378
|
+
return status === 404 ? null : data ?? null;
|
|
5379
|
+
}
|
|
5380
|
+
const d = db || getDatabase();
|
|
5381
|
+
const resolvedId = resolvePartialId(d, "memories", id) ?? id;
|
|
5382
|
+
const row = d.query("SELECT * FROM memories WHERE id = ?").get(resolvedId);
|
|
5383
|
+
if (!row)
|
|
5384
|
+
return null;
|
|
5385
|
+
return parseMemoryRow(row);
|
|
5386
|
+
}
|
|
5387
|
+
function getMemoryByKey(key, scope, agentId, projectId, sessionId, db, as_of) {
|
|
5388
|
+
if (!db && isApiMode()) {
|
|
5389
|
+
const q = toQuery({ key, scope, agent_id: agentId, project_id: projectId, session_id: sessionId, as_of, status: "active", limit: 1 });
|
|
5390
|
+
const { data } = apiJson("GET", `/memories${q}`);
|
|
5391
|
+
return data?.memories?.[0] ?? null;
|
|
5392
|
+
}
|
|
5393
|
+
const d = db || getDatabase();
|
|
5394
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
5395
|
+
const params = [key];
|
|
5396
|
+
if (scope) {
|
|
5397
|
+
sql += " AND scope = ?";
|
|
5398
|
+
params.push(scope);
|
|
5399
|
+
}
|
|
5400
|
+
if (agentId) {
|
|
5401
|
+
sql += " AND agent_id = ?";
|
|
5402
|
+
params.push(agentId);
|
|
5403
|
+
}
|
|
5404
|
+
if (projectId) {
|
|
5405
|
+
sql += " AND project_id = ?";
|
|
5406
|
+
params.push(projectId);
|
|
5407
|
+
}
|
|
5408
|
+
if (sessionId) {
|
|
5409
|
+
sql += " AND session_id = ?";
|
|
5410
|
+
params.push(sessionId);
|
|
5411
|
+
}
|
|
5412
|
+
if (as_of) {
|
|
5413
|
+
sql += " AND (valid_from IS NULL OR valid_from <= ?)";
|
|
5414
|
+
params.push(as_of);
|
|
5415
|
+
sql += " AND (valid_until IS NULL OR valid_until > ?)";
|
|
5416
|
+
params.push(as_of);
|
|
5417
|
+
}
|
|
5418
|
+
sql += " AND status = 'active' ORDER BY importance DESC LIMIT 1";
|
|
5419
|
+
const row = d.query(sql).get(...params);
|
|
5420
|
+
if (!row)
|
|
5421
|
+
return null;
|
|
5422
|
+
return parseMemoryRow(row);
|
|
5423
|
+
}
|
|
5424
|
+
function getMemoriesByKey(key, scope, agentId, projectId, db) {
|
|
5425
|
+
if (!db && isApiMode()) {
|
|
5426
|
+
const q = toQuery({ key, scope, agent_id: agentId, project_id: projectId, status: "active" });
|
|
5427
|
+
const { data } = apiJson("GET", `/memories${q}`);
|
|
5428
|
+
return data?.memories ?? [];
|
|
5429
|
+
}
|
|
5430
|
+
const d = db || getDatabase();
|
|
5431
|
+
let sql = "SELECT * FROM memories WHERE key = ?";
|
|
5432
|
+
const params = [key];
|
|
5433
|
+
if (scope) {
|
|
5434
|
+
sql += " AND scope = ?";
|
|
5435
|
+
params.push(scope);
|
|
5436
|
+
}
|
|
5437
|
+
if (agentId) {
|
|
5438
|
+
sql += " AND agent_id = ?";
|
|
5439
|
+
params.push(agentId);
|
|
5440
|
+
}
|
|
5441
|
+
if (projectId) {
|
|
5442
|
+
sql += " AND project_id = ?";
|
|
5443
|
+
params.push(projectId);
|
|
5444
|
+
}
|
|
5445
|
+
sql += " AND status = 'active' ORDER BY importance DESC";
|
|
5446
|
+
const rows = d.query(sql).all(...params);
|
|
5447
|
+
return rows.map(parseMemoryRow);
|
|
5448
|
+
}
|
|
5449
|
+
function listMemories(filter, db) {
|
|
5450
|
+
if (!db && isApiMode()) {
|
|
5451
|
+
const f = filter || {};
|
|
5452
|
+
const q = toQuery({
|
|
5453
|
+
key: f.key,
|
|
5454
|
+
scope: f.scope,
|
|
5455
|
+
category: f.category,
|
|
5456
|
+
status: f.status,
|
|
5457
|
+
tags: f.tags,
|
|
5458
|
+
min_importance: f.min_importance,
|
|
5459
|
+
pinned: f.pinned,
|
|
5460
|
+
agent_id: f.agent_id,
|
|
5461
|
+
project_id: f.project_id,
|
|
5462
|
+
session_id: f.session_id,
|
|
5463
|
+
namespace: f.namespace,
|
|
5464
|
+
as_of: f.as_of,
|
|
5465
|
+
limit: f.limit,
|
|
5466
|
+
offset: f.offset
|
|
5467
|
+
});
|
|
5468
|
+
const { data } = apiJson("GET", `/memories${q}`);
|
|
5469
|
+
return data?.memories ?? [];
|
|
5470
|
+
}
|
|
5471
|
+
const d = db || getDatabase();
|
|
5472
|
+
const conditions = [];
|
|
5473
|
+
const params = [];
|
|
5474
|
+
if (filter) {
|
|
5475
|
+
if (filter.key) {
|
|
5476
|
+
conditions.push("key = ?");
|
|
5477
|
+
params.push(filter.key);
|
|
5478
|
+
}
|
|
5479
|
+
if (filter.scope) {
|
|
5480
|
+
if (Array.isArray(filter.scope)) {
|
|
5481
|
+
conditions.push(`scope IN (${filter.scope.map(() => "?").join(",")})`);
|
|
5482
|
+
params.push(...filter.scope);
|
|
5483
|
+
} else {
|
|
5484
|
+
conditions.push("scope = ?");
|
|
5485
|
+
params.push(filter.scope);
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
if (filter.category) {
|
|
5489
|
+
if (Array.isArray(filter.category)) {
|
|
5490
|
+
conditions.push(`category IN (${filter.category.map(() => "?").join(",")})`);
|
|
5491
|
+
params.push(...filter.category);
|
|
5492
|
+
} else {
|
|
5493
|
+
conditions.push("category = ?");
|
|
5494
|
+
params.push(filter.category);
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
if (filter.source) {
|
|
5498
|
+
if (Array.isArray(filter.source)) {
|
|
5499
|
+
conditions.push(`source IN (${filter.source.map(() => "?").join(",")})`);
|
|
5500
|
+
params.push(...filter.source);
|
|
5501
|
+
} else {
|
|
5502
|
+
conditions.push("source = ?");
|
|
5503
|
+
params.push(filter.source);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
if (filter.status) {
|
|
5507
|
+
if (Array.isArray(filter.status)) {
|
|
5508
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
5509
|
+
params.push(...filter.status);
|
|
5510
|
+
} else {
|
|
5511
|
+
conditions.push("status = ?");
|
|
5512
|
+
params.push(filter.status);
|
|
5513
|
+
}
|
|
5514
|
+
} else {
|
|
5515
|
+
conditions.push("status = 'active'");
|
|
5516
|
+
}
|
|
5517
|
+
if (filter.project_id) {
|
|
5518
|
+
conditions.push("project_id = ?");
|
|
5519
|
+
params.push(filter.project_id);
|
|
5520
|
+
}
|
|
5521
|
+
if (filter.agent_id) {
|
|
5522
|
+
conditions.push("agent_id = ?");
|
|
5523
|
+
params.push(filter.agent_id);
|
|
5524
|
+
}
|
|
5525
|
+
if (filter.session_id) {
|
|
5526
|
+
conditions.push("session_id = ?");
|
|
5527
|
+
params.push(filter.session_id);
|
|
5528
|
+
}
|
|
5529
|
+
if ("machine_id" in filter) {
|
|
5530
|
+
if (filter.machine_id === null) {
|
|
5531
|
+
conditions.push("machine_id IS NULL");
|
|
5532
|
+
} else if (filter.machine_id) {
|
|
5533
|
+
conditions.push("machine_id = ?");
|
|
5534
|
+
params.push(filter.machine_id);
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
if ("visible_to_machine_id" in filter) {
|
|
5538
|
+
if (filter.visible_to_machine_id === null) {
|
|
5539
|
+
conditions.push("machine_id IS NULL");
|
|
5540
|
+
} else if (filter.visible_to_machine_id !== undefined) {
|
|
5541
|
+
conditions.push("(machine_id IS NULL OR machine_id = ?)");
|
|
5542
|
+
params.push(filter.visible_to_machine_id);
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
if (filter.min_importance) {
|
|
5546
|
+
conditions.push("importance >= ?");
|
|
5547
|
+
params.push(filter.min_importance);
|
|
5548
|
+
}
|
|
5549
|
+
if (filter.pinned !== undefined) {
|
|
5550
|
+
conditions.push("pinned = ?");
|
|
5551
|
+
params.push(filter.pinned ? 1 : 0);
|
|
5552
|
+
}
|
|
5553
|
+
if (filter.flagged === true) {
|
|
5554
|
+
conditions.push("flag IS NOT NULL");
|
|
5555
|
+
} else if (filter.flag) {
|
|
5556
|
+
conditions.push("flag = ?");
|
|
5557
|
+
params.push(filter.flag);
|
|
5558
|
+
}
|
|
5559
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
5560
|
+
for (const tag of filter.tags) {
|
|
5561
|
+
conditions.push("id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)");
|
|
5562
|
+
params.push(tag);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
if (filter.namespace) {
|
|
5566
|
+
conditions.push("namespace = ?");
|
|
5567
|
+
params.push(filter.namespace);
|
|
5568
|
+
}
|
|
5569
|
+
if (filter.search) {
|
|
5570
|
+
conditions.push("(key LIKE ? OR value LIKE ? OR summary LIKE ?)");
|
|
5571
|
+
const term = `%${filter.search}%`;
|
|
5572
|
+
params.push(term, term, term);
|
|
5573
|
+
}
|
|
5574
|
+
if (filter.as_of) {
|
|
5575
|
+
conditions.push("(valid_from IS NULL OR valid_from <= ?)");
|
|
5576
|
+
params.push(filter.as_of);
|
|
5577
|
+
conditions.push("(valid_until IS NULL OR valid_until > ?)");
|
|
5578
|
+
params.push(filter.as_of);
|
|
5579
|
+
}
|
|
5580
|
+
} else {
|
|
5581
|
+
conditions.push("status = 'active'");
|
|
5582
|
+
}
|
|
5583
|
+
let sql = "SELECT * FROM memories";
|
|
5584
|
+
if (conditions.length > 0) {
|
|
5585
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
5586
|
+
}
|
|
5587
|
+
sql += " ORDER BY importance DESC, created_at DESC";
|
|
5588
|
+
if (filter?.limit) {
|
|
5589
|
+
sql += " LIMIT ?";
|
|
5590
|
+
params.push(filter.limit);
|
|
5591
|
+
}
|
|
5592
|
+
if (filter?.offset) {
|
|
5593
|
+
sql += " OFFSET ?";
|
|
5594
|
+
params.push(filter.offset);
|
|
5595
|
+
}
|
|
5596
|
+
const rows = d.query(sql).all(...params);
|
|
5597
|
+
return rows.map(parseMemoryRow);
|
|
5598
|
+
}
|
|
5599
|
+
function updateMemory(id, input, db) {
|
|
5600
|
+
if (!db && isApiMode()) {
|
|
5601
|
+
const { status, data } = apiJson("PATCH", `/memories/${encodeURIComponent(id)}`, input, { allow404: true });
|
|
5602
|
+
if (status === 404)
|
|
5603
|
+
throw new MemoryNotFoundError(id);
|
|
5604
|
+
if (data && typeof input.version === "number" && typeof data.version === "number" && data.version <= input.version) {
|
|
5605
|
+
throw new Error(`Update did not persist for memory ${id}: the server returned success but the record is unchanged ` + `(version still ${data.version}). Your data was NOT written. ` + `The server is likely running a build predating the partial-id fix \u2014 pass the full 36-character id as a workaround.`);
|
|
5606
|
+
}
|
|
5607
|
+
return data;
|
|
5608
|
+
}
|
|
5609
|
+
const d = db || getDatabase();
|
|
5610
|
+
const existing = getMemory(id, d);
|
|
5611
|
+
if (!existing)
|
|
5612
|
+
throw new MemoryNotFoundError(id);
|
|
5613
|
+
const memoryId = existing.id;
|
|
5614
|
+
if (existing.version !== input.version) {
|
|
5615
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
5616
|
+
}
|
|
5617
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
5618
|
+
const params = [now()];
|
|
5619
|
+
if (input.value !== undefined) {
|
|
5620
|
+
sets.push("value = ?");
|
|
5621
|
+
params.push(redactSecrets(input.value));
|
|
5622
|
+
}
|
|
5623
|
+
if (input.category !== undefined) {
|
|
5624
|
+
sets.push("category = ?");
|
|
5625
|
+
params.push(input.category);
|
|
5626
|
+
}
|
|
5627
|
+
if (input.scope !== undefined) {
|
|
5628
|
+
sets.push("scope = ?");
|
|
5629
|
+
params.push(input.scope);
|
|
5630
|
+
}
|
|
5631
|
+
if (input.summary !== undefined) {
|
|
5632
|
+
sets.push("summary = ?");
|
|
5633
|
+
params.push(input.summary);
|
|
5634
|
+
}
|
|
5635
|
+
if (input.importance !== undefined) {
|
|
5636
|
+
sets.push("importance = ?");
|
|
5637
|
+
params.push(input.importance);
|
|
5638
|
+
}
|
|
5639
|
+
if (input.pinned !== undefined) {
|
|
5640
|
+
sets.push("pinned = ?");
|
|
5641
|
+
params.push(input.pinned ? 1 : 0);
|
|
5642
|
+
}
|
|
5643
|
+
if (input.status !== undefined) {
|
|
5644
|
+
sets.push("status = ?");
|
|
5645
|
+
params.push(input.status);
|
|
5646
|
+
}
|
|
5647
|
+
if (input.metadata !== undefined) {
|
|
5648
|
+
sets.push("metadata = ?");
|
|
5649
|
+
params.push(JSON.stringify(input.metadata));
|
|
5650
|
+
}
|
|
5651
|
+
if (input.expires_at !== undefined) {
|
|
5652
|
+
sets.push("expires_at = ?");
|
|
5653
|
+
params.push(input.expires_at);
|
|
5654
|
+
}
|
|
5655
|
+
if (input.flag !== undefined) {
|
|
5656
|
+
sets.push("flag = ?");
|
|
5657
|
+
params.push(input.flag ?? null);
|
|
5658
|
+
}
|
|
5659
|
+
if (input.when_to_use !== undefined) {
|
|
5660
|
+
sets.push("when_to_use = ?");
|
|
5661
|
+
params.push(input.when_to_use ?? null);
|
|
5662
|
+
}
|
|
5663
|
+
if (input.tags !== undefined) {
|
|
5664
|
+
sets.push("tags = ?");
|
|
5665
|
+
params.push(JSON.stringify(input.tags));
|
|
5666
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memoryId]);
|
|
5667
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
5668
|
+
for (const tag of input.tags) {
|
|
5669
|
+
insertTag.run(memoryId, tag);
|
|
5670
|
+
}
|
|
5671
|
+
}
|
|
5672
|
+
params.push(memoryId);
|
|
5673
|
+
const result = d.run(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5674
|
+
if (result.changes === 0) {
|
|
5675
|
+
throw new Error(`Update affected no rows for memory ${memoryId}: the record was read but not written. ` + `This is a bug in @hasna/mementos, not a bad argument \u2014 please report it.`);
|
|
5676
|
+
}
|
|
5677
|
+
const updated = getMemory(memoryId, d);
|
|
5678
|
+
if (input.value !== undefined) {
|
|
5679
|
+
try {
|
|
5680
|
+
const oldLinks = getEntityMemoryLinks(undefined, updated.id, d);
|
|
5681
|
+
for (const link of oldLinks) {
|
|
5682
|
+
unlinkEntityFromMemory(link.entity_id, updated.id, d);
|
|
5683
|
+
}
|
|
5684
|
+
} catch {}
|
|
5685
|
+
}
|
|
5686
|
+
hookRegistry.runHooks("PostMemoryUpdate", {
|
|
5687
|
+
memory: updated,
|
|
5688
|
+
previousValue: existing.value,
|
|
5689
|
+
agentId: existing.agent_id ?? undefined,
|
|
5690
|
+
projectId: existing.project_id ?? undefined,
|
|
5691
|
+
sessionId: existing.session_id ?? undefined,
|
|
5692
|
+
timestamp: Date.now()
|
|
5693
|
+
});
|
|
5694
|
+
return updated;
|
|
5695
|
+
}
|
|
5696
|
+
function deleteMemory(id, db) {
|
|
5697
|
+
if (!db && isApiMode()) {
|
|
5698
|
+
const { status } = apiJson("DELETE", `/memories/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
5699
|
+
return status !== 404;
|
|
5700
|
+
}
|
|
5701
|
+
const d = db || getDatabase();
|
|
5702
|
+
const memoryId = resolvePartialId(d, "memories", id) ?? id;
|
|
5703
|
+
const result = d.run("DELETE FROM memories WHERE id = ?", [memoryId]);
|
|
5704
|
+
if (result.changes > 0) {
|
|
5705
|
+
hookRegistry.runHooks("PostMemoryDelete", {
|
|
5706
|
+
memoryId,
|
|
5707
|
+
timestamp: Date.now()
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5710
|
+
return result.changes > 0;
|
|
5711
|
+
}
|
|
5712
|
+
function bulkDeleteMemories(ids, db) {
|
|
5713
|
+
if (ids.length === 0)
|
|
5714
|
+
return 0;
|
|
5715
|
+
if (!db && isApiMode()) {
|
|
5716
|
+
const { data } = apiJson("POST", "/memories/bulk-forget", { ids });
|
|
5717
|
+
return data?.deleted ?? 0;
|
|
5718
|
+
}
|
|
5719
|
+
const d = db || getDatabase();
|
|
5720
|
+
const resolvedIds = ids.map((id) => resolvePartialId(d, "memories", id) ?? id);
|
|
5721
|
+
const placeholders = resolvedIds.map(() => "?").join(",");
|
|
5722
|
+
const countRow = d.query(`SELECT COUNT(*) as c FROM memories WHERE id IN (${placeholders})`).get(...resolvedIds);
|
|
5723
|
+
const count = countRow.c;
|
|
5724
|
+
if (count > 0) {
|
|
5725
|
+
d.run(`DELETE FROM memories WHERE id IN (${placeholders})`, resolvedIds);
|
|
5726
|
+
}
|
|
5727
|
+
return count;
|
|
5728
|
+
}
|
|
5729
|
+
function touchMemory(id, db) {
|
|
5730
|
+
if (!db && isApiMode())
|
|
5731
|
+
return;
|
|
5732
|
+
const d = db || getDatabase();
|
|
5733
|
+
d.run("UPDATE memories SET access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
5734
|
+
}
|
|
5735
|
+
var RECALL_PROMOTE_THRESHOLD = 3;
|
|
5736
|
+
function incrementRecallCount(id, db) {
|
|
5737
|
+
if (!db && isApiMode())
|
|
5738
|
+
return;
|
|
5739
|
+
const d = db || getDatabase();
|
|
5740
|
+
try {
|
|
5741
|
+
d.run("UPDATE memories SET recall_count = recall_count + 1, access_count = access_count + 1, accessed_at = ? WHERE id = ?", [now(), id]);
|
|
5742
|
+
const row = d.query("SELECT recall_count, importance FROM memories WHERE id = ?").get(id);
|
|
5743
|
+
if (!row)
|
|
5744
|
+
return;
|
|
5745
|
+
const promotions = Math.floor(row.recall_count / RECALL_PROMOTE_THRESHOLD);
|
|
5746
|
+
if (promotions > 0 && row.importance < 10) {
|
|
5747
|
+
const newImportance = Math.min(10, row.importance + 1);
|
|
5748
|
+
d.run("UPDATE memories SET importance = ? WHERE id = ? AND importance < 10", [newImportance, id]);
|
|
5749
|
+
}
|
|
5750
|
+
} catch {}
|
|
5751
|
+
}
|
|
5752
|
+
function cleanExpiredMemories(db) {
|
|
5753
|
+
if (!db && isApiMode()) {
|
|
5754
|
+
const { data } = apiJson("POST", "/memories/clean");
|
|
5755
|
+
return data?.cleaned ?? 0;
|
|
5756
|
+
}
|
|
5757
|
+
const d = db || getDatabase();
|
|
5758
|
+
const timestamp = now();
|
|
5759
|
+
const countRow = d.query("SELECT COUNT(*) as c FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?").get(timestamp);
|
|
5760
|
+
const count = countRow.c;
|
|
5761
|
+
if (count > 0) {
|
|
5762
|
+
d.run("DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?", [timestamp]);
|
|
5763
|
+
}
|
|
5764
|
+
return count;
|
|
5765
|
+
}
|
|
5766
|
+
function getMemoryVersions(memoryId, db) {
|
|
5767
|
+
if (!db && isApiMode()) {
|
|
5768
|
+
const { data } = apiJson("GET", `/memories/${encodeURIComponent(memoryId)}/versions`);
|
|
5769
|
+
return data?.versions ?? [];
|
|
5770
|
+
}
|
|
5771
|
+
const d = db || getDatabase();
|
|
5772
|
+
try {
|
|
5773
|
+
const rows = d.query("SELECT * FROM memory_versions WHERE memory_id = ? ORDER BY version ASC").all(memoryId);
|
|
5774
|
+
return rows.map((row) => ({
|
|
5775
|
+
id: row["id"],
|
|
5776
|
+
memory_id: row["memory_id"],
|
|
5777
|
+
version: row["version"],
|
|
5778
|
+
value: row["value"],
|
|
5779
|
+
importance: row["importance"],
|
|
5780
|
+
scope: row["scope"],
|
|
5781
|
+
category: row["category"],
|
|
5782
|
+
tags: JSON.parse(row["tags"] || "[]"),
|
|
5783
|
+
summary: row["summary"] || null,
|
|
5784
|
+
pinned: !!row["pinned"],
|
|
5785
|
+
status: row["status"],
|
|
5786
|
+
created_at: row["created_at"]
|
|
5787
|
+
}));
|
|
5788
|
+
} catch {
|
|
5789
|
+
return [];
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5793
|
+
// src/project-registration/project-resources.ts
|
|
5794
|
+
var DEFAULT_PAGE_LIMIT = 100;
|
|
5795
|
+
var MAX_PAGE_LIMIT = 1000;
|
|
5796
|
+
var CURSOR_SCHEMA = "mementos.project-resources.cursor.v1";
|
|
5797
|
+
|
|
5798
|
+
class MementosProjectResourceError extends Error {
|
|
5799
|
+
code;
|
|
5800
|
+
details;
|
|
5801
|
+
constructor(code, message, details = {}) {
|
|
5802
|
+
super(message);
|
|
5803
|
+
this.code = code;
|
|
5804
|
+
this.details = details;
|
|
5805
|
+
this.name = "MementosProjectResourceError";
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
function timestamp(value) {
|
|
5809
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
5810
|
+
}
|
|
5811
|
+
function normalizeSqlValue(value) {
|
|
5812
|
+
if (value instanceof Date)
|
|
5813
|
+
return value.toISOString();
|
|
5814
|
+
if (Array.isArray(value))
|
|
5815
|
+
return value.map(normalizeSqlValue);
|
|
5816
|
+
if (!value || typeof value !== "object")
|
|
5817
|
+
return value;
|
|
5818
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeSqlValue(item)]));
|
|
5819
|
+
}
|
|
5820
|
+
function exactProject(db, projectId) {
|
|
5821
|
+
const row = db.get("SELECT id, name, path, description, memory_prefix, created_at, updated_at FROM projects WHERE id = ? LIMIT 1", projectId);
|
|
5822
|
+
if (!row) {
|
|
5823
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_PROJECT_NOT_FOUND", `Mementos project not found: ${projectId}`, { project_id: projectId });
|
|
5824
|
+
}
|
|
5825
|
+
return row;
|
|
5826
|
+
}
|
|
5827
|
+
function resourceKey(resource) {
|
|
5828
|
+
const rank = MEMENTOS_PROJECT_RESOURCE_KINDS.indexOf(resource.resource_kind);
|
|
5829
|
+
return `${String(rank).padStart(2, "0")}:${resource.stable_id}`;
|
|
5830
|
+
}
|
|
5831
|
+
function projectResource(project) {
|
|
5832
|
+
const normalized = normalizeSqlValue(project);
|
|
5833
|
+
return {
|
|
5834
|
+
authority: "mementos",
|
|
5835
|
+
source_package: "@hasna/mementos",
|
|
5836
|
+
project_id: project.id,
|
|
5837
|
+
resource_kind: "project",
|
|
5838
|
+
stable_id: project.id,
|
|
5839
|
+
revision: timestamp(project.updated_at),
|
|
5840
|
+
digest: digestMementosProjectRegistrationValue(normalized),
|
|
5841
|
+
membership: "project_aggregate"
|
|
5842
|
+
};
|
|
5843
|
+
}
|
|
5844
|
+
function memoryResources(db, projectId) {
|
|
5845
|
+
const rows = db.all("SELECT * FROM memories WHERE project_id = ? ORDER BY id ASC", projectId);
|
|
5846
|
+
return rows.map((row) => {
|
|
5847
|
+
const memory = normalizeSqlValue(parseMemoryRow(row));
|
|
5848
|
+
return {
|
|
5849
|
+
authority: "mementos",
|
|
5850
|
+
source_package: "@hasna/mementos",
|
|
5851
|
+
project_id: projectId,
|
|
5852
|
+
resource_kind: row["category"] === "knowledge" ? "knowledge" : "memory",
|
|
5853
|
+
stable_id: String(row["id"]),
|
|
5854
|
+
revision: timestamp(row["updated_at"]),
|
|
5855
|
+
digest: digestMementosProjectRegistrationValue(memory),
|
|
5856
|
+
membership: "explicit_project_id_or_focus"
|
|
5857
|
+
};
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
function sessionResources(db, projectId) {
|
|
5861
|
+
const rows = db.all("SELECT * FROM session_memory_jobs WHERE project_id = ? ORDER BY id ASC", projectId);
|
|
5862
|
+
return rows.map((row) => {
|
|
5863
|
+
const normalized = {
|
|
5864
|
+
id: String(row["id"]),
|
|
5865
|
+
session_id: String(row["session_id"]),
|
|
5866
|
+
agent_id: row["agent_id"] === null ? null : String(row["agent_id"] ?? "") || null,
|
|
5867
|
+
project_id: row["project_id"] === null ? null : String(row["project_id"] ?? "") || null,
|
|
5868
|
+
source: String(row["source"]),
|
|
5869
|
+
status: String(row["status"]),
|
|
5870
|
+
transcript: String(row["transcript"]),
|
|
5871
|
+
chunk_count: Number(row["chunk_count"]),
|
|
5872
|
+
memories_extracted: Number(row["memories_extracted"]),
|
|
5873
|
+
error: row["error"] === null ? null : String(row["error"] ?? "") || null,
|
|
5874
|
+
metadata: typeof row["metadata"] === "string" ? JSON.parse(row["metadata"] || "{}") : normalizeSqlValue(row["metadata"] ?? {}),
|
|
5875
|
+
created_at: timestamp(row["created_at"]),
|
|
5876
|
+
started_at: row["started_at"] === null ? null : timestamp(row["started_at"]),
|
|
5877
|
+
completed_at: row["completed_at"] === null ? null : timestamp(row["completed_at"])
|
|
5878
|
+
};
|
|
5879
|
+
return {
|
|
5880
|
+
authority: "mementos",
|
|
5881
|
+
source_package: "@hasna/mementos",
|
|
5882
|
+
project_id: projectId,
|
|
5883
|
+
resource_kind: "session",
|
|
5884
|
+
stable_id: String(row["id"]),
|
|
5885
|
+
revision: timestamp(row["completed_at"] ?? row["started_at"] ?? row["created_at"]),
|
|
5886
|
+
digest: digestMementosProjectRegistrationValue(normalized),
|
|
5887
|
+
membership: "explicit_project_id_or_focus"
|
|
5888
|
+
};
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
function normalizeResourceKinds(value) {
|
|
5892
|
+
if (!value)
|
|
5893
|
+
return [...MEMENTOS_PROJECT_RESOURCE_KINDS];
|
|
5894
|
+
if (value.length === 0) {
|
|
5895
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "resource_kinds must contain at least one supported resource kind");
|
|
5896
|
+
}
|
|
5897
|
+
const requested = new Set(value);
|
|
5898
|
+
for (const kind of requested) {
|
|
5899
|
+
if (!MEMENTOS_PROJECT_RESOURCE_KINDS.includes(kind)) {
|
|
5900
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `Unsupported Mementos project resource kind: ${kind}`);
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
return MEMENTOS_PROJECT_RESOURCE_KINDS.filter((kind) => requested.has(kind));
|
|
5904
|
+
}
|
|
5905
|
+
function normalizeLimit(value) {
|
|
5906
|
+
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
5907
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
|
|
5908
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `limit must be an integer between 1 and ${MAX_PAGE_LIMIT}`);
|
|
5909
|
+
}
|
|
5910
|
+
return limit;
|
|
5911
|
+
}
|
|
5912
|
+
function encodeCursor(cursor) {
|
|
5913
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
5914
|
+
}
|
|
5915
|
+
function decodeCursor(raw) {
|
|
5916
|
+
try {
|
|
5917
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
5918
|
+
if (parsed.schema !== CURSOR_SCHEMA || typeof parsed.project_id !== "string" || typeof parsed.collection_revision !== "string" || !Array.isArray(parsed.resource_kinds) || typeof parsed.after_key !== "string") {
|
|
5919
|
+
throw new Error("invalid cursor shape");
|
|
5920
|
+
}
|
|
5921
|
+
return parsed;
|
|
5922
|
+
} catch {
|
|
5923
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "cursor is not a valid Mementos project-resource cursor");
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
function localPopulation(projectId, db, resourceKinds) {
|
|
5927
|
+
const project = exactProject(db, projectId);
|
|
5928
|
+
const selected = new Set(resourceKinds);
|
|
5929
|
+
const resources = [
|
|
5930
|
+
...selected.has("project") ? [projectResource(project)] : [],
|
|
5931
|
+
...memoryResources(db, projectId).filter((resource) => selected.has(resource.resource_kind)),
|
|
5932
|
+
...selected.has("session") ? sessionResources(db, projectId) : []
|
|
5933
|
+
].sort((left, right) => resourceKey(left).localeCompare(resourceKey(right)));
|
|
5934
|
+
const collectionRevision = digestMementosProjectRegistrationValue({
|
|
5935
|
+
schema: MEMENTOS_PROJECT_RESOURCE_ROUTE,
|
|
5936
|
+
project_id: projectId,
|
|
5937
|
+
project_revision: timestamp(project.updated_at),
|
|
5938
|
+
resource_kinds: resourceKinds,
|
|
5939
|
+
resources: resources.map((resource) => ({
|
|
5940
|
+
resource_kind: resource.resource_kind,
|
|
5941
|
+
stable_id: resource.stable_id,
|
|
5942
|
+
revision: resource.revision,
|
|
5943
|
+
digest: resource.digest
|
|
5944
|
+
}))
|
|
5945
|
+
});
|
|
5946
|
+
return { project, resources, collectionRevision };
|
|
5947
|
+
}
|
|
5948
|
+
function readMementosProjectResourcePage(projectId, options = {}, db, authorityOptions = {}) {
|
|
5949
|
+
const resourceKinds = normalizeResourceKinds(options.resource_kinds);
|
|
5950
|
+
const limit = normalizeLimit(options.limit);
|
|
5951
|
+
if (!db && isApiMode()) {
|
|
5952
|
+
const { data } = apiJson("GET", `/projects/${encodeURIComponent(projectId)}/resources${toQuery({
|
|
5953
|
+
limit,
|
|
5954
|
+
cursor: options.cursor ?? undefined,
|
|
5955
|
+
resource_kinds: resourceKinds.join(",")
|
|
5956
|
+
})}`);
|
|
5957
|
+
return data;
|
|
5958
|
+
}
|
|
5959
|
+
const d = db ?? getDatabase();
|
|
5960
|
+
const { project, resources, collectionRevision } = localPopulation(projectId, d, resourceKinds);
|
|
5961
|
+
let start = 0;
|
|
5962
|
+
if (options.cursor) {
|
|
5963
|
+
const cursor = decodeCursor(options.cursor);
|
|
5964
|
+
if (cursor.project_id !== projectId || JSON.stringify(cursor.resource_kinds) !== JSON.stringify(resourceKinds)) {
|
|
5965
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", "cursor does not belong to this project and resource-kind selection");
|
|
5966
|
+
}
|
|
5967
|
+
if (cursor.collection_revision !== collectionRevision) {
|
|
5968
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_COLLECTION_CHANGED", "Mementos project resource collection changed; restart from the first page", {
|
|
5969
|
+
cursor_collection_revision: cursor.collection_revision,
|
|
5970
|
+
current_collection_revision: collectionRevision
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
const afterIndex = resources.findIndex((resource) => resourceKey(resource) === cursor.after_key);
|
|
5974
|
+
if (afterIndex < 0) {
|
|
5975
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_COLLECTION_CHANGED", "Mementos project resource cursor no longer names a member; restart from the first page");
|
|
5976
|
+
}
|
|
5977
|
+
start = afterIndex + 1;
|
|
5978
|
+
}
|
|
5979
|
+
const pageResources = resources.slice(start, start + limit);
|
|
5980
|
+
const hasMore = start + pageResources.length < resources.length;
|
|
5981
|
+
const nextCursor = hasMore && pageResources.length > 0 ? encodeCursor({
|
|
5982
|
+
schema: CURSOR_SCHEMA,
|
|
5983
|
+
project_id: projectId,
|
|
5984
|
+
collection_revision: collectionRevision,
|
|
5985
|
+
resource_kinds: resourceKinds,
|
|
5986
|
+
after_key: resourceKey(pageResources[pageResources.length - 1])
|
|
5987
|
+
}) : null;
|
|
5988
|
+
const capability = buildMementosProjectRegistrationCapability(authorityOptions);
|
|
5989
|
+
return {
|
|
5990
|
+
schema: "mementos.project-resources.v1",
|
|
5991
|
+
authority: {
|
|
5992
|
+
authority: capability.authority,
|
|
5993
|
+
authority_id: capability.authority_id,
|
|
5994
|
+
tenant_id: capability.tenant_id,
|
|
5995
|
+
corpus_id: capability.corpus_id,
|
|
5996
|
+
package_version: capability.package_version
|
|
5997
|
+
},
|
|
5998
|
+
project_id: projectId,
|
|
5999
|
+
project_revision: timestamp(project.updated_at),
|
|
6000
|
+
collection_revision: collectionRevision,
|
|
6001
|
+
resource_kinds: resourceKinds,
|
|
6002
|
+
resources: pageResources,
|
|
6003
|
+
count: pageResources.length,
|
|
6004
|
+
total: resources.length,
|
|
6005
|
+
limit,
|
|
6006
|
+
cursor: options.cursor ?? null,
|
|
6007
|
+
next_cursor: nextCursor,
|
|
6008
|
+
has_more: hasMore,
|
|
6009
|
+
complete: true,
|
|
6010
|
+
truncated: false
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
function readAllMementosProjectResources(projectId, options = {}, db, authorityOptions = {}) {
|
|
6014
|
+
const pageSize = normalizeLimit(options.page_size);
|
|
6015
|
+
let cursor = null;
|
|
6016
|
+
let first = null;
|
|
6017
|
+
const resources = [];
|
|
6018
|
+
const seen = new Set;
|
|
6019
|
+
do {
|
|
6020
|
+
const page = readMementosProjectResourcePage(projectId, {
|
|
6021
|
+
limit: pageSize,
|
|
6022
|
+
cursor,
|
|
6023
|
+
resource_kinds: options.resource_kinds
|
|
6024
|
+
}, db, authorityOptions);
|
|
6025
|
+
if (!first)
|
|
6026
|
+
first = page;
|
|
6027
|
+
if (page.collection_revision !== first.collection_revision || page.total !== first.total || JSON.stringify(page.resource_kinds) !== JSON.stringify(first.resource_kinds)) {
|
|
6028
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_COLLECTION_CHANGED", "Mementos project resource collection changed during complete traversal");
|
|
6029
|
+
}
|
|
6030
|
+
for (const resource of page.resources) {
|
|
6031
|
+
const key = resourceKey(resource);
|
|
6032
|
+
if (seen.has(key)) {
|
|
6033
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INCOMPLETE", `Mementos project resource traversal returned duplicate stable ID: ${key}`);
|
|
6034
|
+
}
|
|
6035
|
+
seen.add(key);
|
|
6036
|
+
resources.push(resource);
|
|
6037
|
+
}
|
|
6038
|
+
if (page.has_more && !page.next_cursor) {
|
|
6039
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INCOMPLETE", "Mementos project resource page claimed more results without a continuation cursor");
|
|
6040
|
+
}
|
|
6041
|
+
cursor = page.next_cursor;
|
|
6042
|
+
} while (cursor);
|
|
6043
|
+
if (!first) {
|
|
6044
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INCOMPLETE", "Mementos project resource traversal returned no first page");
|
|
6045
|
+
}
|
|
6046
|
+
if (resources.length !== first.total) {
|
|
6047
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INCOMPLETE", `Mementos project resource traversal returned ${resources.length} of ${first.total} resources`);
|
|
6048
|
+
}
|
|
6049
|
+
return {
|
|
6050
|
+
...first,
|
|
6051
|
+
resources,
|
|
6052
|
+
count: resources.length,
|
|
6053
|
+
total: resources.length,
|
|
6054
|
+
limit: pageSize,
|
|
6055
|
+
cursor: null,
|
|
6056
|
+
next_cursor: null,
|
|
6057
|
+
has_more: false,
|
|
6058
|
+
complete: true,
|
|
6059
|
+
truncated: false
|
|
6060
|
+
};
|
|
6061
|
+
}
|
|
6062
|
+
function getMementosProjectResourceExact(projectId, resourceKind, stableId, db, authorityOptions = {}) {
|
|
6063
|
+
if (!MEMENTOS_PROJECT_RESOURCE_KINDS.includes(resourceKind)) {
|
|
6064
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_INVALID_INPUT", `Unsupported Mementos project resource kind: ${resourceKind}`);
|
|
6065
|
+
}
|
|
6066
|
+
if (!db && isApiMode()) {
|
|
6067
|
+
const { data } = apiJson("GET", `/projects/${encodeURIComponent(projectId)}/resources/${encodeURIComponent(resourceKind)}/${encodeURIComponent(stableId)}`);
|
|
6068
|
+
return data;
|
|
6069
|
+
}
|
|
6070
|
+
const d = db ?? getDatabase();
|
|
6071
|
+
const { project, resources, collectionRevision } = localPopulation(projectId, d, [
|
|
6072
|
+
resourceKind
|
|
6073
|
+
]);
|
|
6074
|
+
const resource = resources.find((candidate) => candidate.stable_id === stableId);
|
|
6075
|
+
if (!resource) {
|
|
6076
|
+
throw new MementosProjectResourceError("MEMENTOS_PROJECT_RESOURCE_NOT_FOUND", `Mementos ${resourceKind} resource not found in project ${projectId}: ${stableId}`, { project_id: projectId, resource_kind: resourceKind, stable_id: stableId });
|
|
6077
|
+
}
|
|
6078
|
+
const capability = buildMementosProjectRegistrationCapability(authorityOptions);
|
|
6079
|
+
return {
|
|
6080
|
+
schema: "mementos.project-resource.v1",
|
|
6081
|
+
authority: {
|
|
6082
|
+
authority: capability.authority,
|
|
6083
|
+
authority_id: capability.authority_id,
|
|
6084
|
+
tenant_id: capability.tenant_id,
|
|
6085
|
+
corpus_id: capability.corpus_id,
|
|
6086
|
+
package_version: capability.package_version
|
|
6087
|
+
},
|
|
6088
|
+
project_id: projectId,
|
|
6089
|
+
project_revision: timestamp(project.updated_at),
|
|
6090
|
+
collection_revision: collectionRevision,
|
|
6091
|
+
resource,
|
|
6092
|
+
complete: true,
|
|
6093
|
+
truncated: false
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
4673
6096
|
export {
|
|
4674
6097
|
sqliteMementosProjectRegistrationSchemaSql,
|
|
4675
6098
|
sqliteMementosProjectGuardedUpdateSchemaSql,
|
|
6099
|
+
resolveMementosProjectAuthorityIdentity,
|
|
6100
|
+
readMementosProjectResourcePage,
|
|
6101
|
+
readAllMementosProjectResources,
|
|
4676
6102
|
postgresMementosProjectRegistrationSchemaSql,
|
|
4677
6103
|
postgresMementosProjectGuardedUpdateSchemaSql,
|
|
4678
6104
|
mementosProjectReferenceCounts,
|
|
4679
6105
|
hasMementosProjectReferences,
|
|
4680
6106
|
handleMementosProjectRegistrationHttpRequest,
|
|
6107
|
+
getMementosProjectResourceExact,
|
|
4681
6108
|
digestMementosProjectRegistrationValue,
|
|
4682
6109
|
deriveMementosProjectRegistrationIdempotencyKey,
|
|
4683
6110
|
createMementosProjectRegistrationHttpClient,
|
|
4684
6111
|
createMementosProjectRegistrationAuthority,
|
|
4685
6112
|
createLocalMementosProjectRegistrationAuthority,
|
|
4686
6113
|
canonicalMementosProjectRegistrationJson,
|
|
6114
|
+
buildMementosProjectRegistrationCapability,
|
|
4687
6115
|
PackageOwnedMementosProjectRegistrationAuthority,
|
|
6116
|
+
MementosProjectResourceError,
|
|
4688
6117
|
MementosProjectRegistrationHttpClient,
|
|
4689
6118
|
MementosProjectRegistrationError,
|
|
6119
|
+
MementosProjectAuthorityIdentityError,
|
|
6120
|
+
MEMENTOS_PROJECT_RESOURCE_ROUTE,
|
|
6121
|
+
MEMENTOS_PROJECT_RESOURCE_KINDS,
|
|
4690
6122
|
MEMENTOS_PROJECT_REGISTRATION_SCHEMA_VERSION,
|
|
4691
6123
|
MEMENTOS_PROJECT_REGISTRATION_ROUTE,
|
|
4692
6124
|
MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE,
|
|
4693
6125
|
MEMENTOS_PROJECT_REFERENCE_SURFACES,
|
|
4694
|
-
MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE
|
|
6126
|
+
MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
6127
|
+
MEMENTOS_PROJECT_AUTHORITY_ENV
|
|
4695
6128
|
};
|