jamgate 0.1.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/embeddings/embedder.js +57 -0
- package/dist/embeddings/vector.js +54 -0
- package/dist/gate/log.js +77 -0
- package/dist/gate/prefilter.js +17 -0
- package/dist/gate/relevance.js +138 -0
- package/dist/gate/subject.js +73 -0
- package/dist/index.js +201 -0
- package/dist/store/fileStore.js +274 -0
- package/dist/store/lock.js +92 -0
- package/dist/store/schema.js +43 -0
- package/dist/store/ttl.js +85 -0
- package/dist/store/types.js +5 -0
- package/package.json +63 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// On-disk schema versioning + migration (Phase 2, item 4).
|
|
2
|
+
//
|
|
3
|
+
// The store file used to be a bare JSON array of memories with no version marker.
|
|
4
|
+
// Adding fields (expiresAt) and, later, other shape changes means existing users'
|
|
5
|
+
// files must keep working — so every file now carries an explicit `schemaVersion`, and
|
|
6
|
+
// this module upgrades any older shape to the current one on read. Migration is pure
|
|
7
|
+
// and in-memory; the upgraded shape is persisted the next time the store writes.
|
|
8
|
+
import { computeExpiresAt } from "./ttl.js";
|
|
9
|
+
/** Current on-disk schema version.
|
|
10
|
+
* v1 (implicit): a bare `Memory[]`, no version field, no `expiresAt`.
|
|
11
|
+
* v2: a versioned envelope `{ schemaVersion, memories }` with per-record `expiresAt`. */
|
|
12
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
13
|
+
/** Parse-and-migrate any recognized on-disk shape to the current in-memory envelope.
|
|
14
|
+
* Unrecognized/empty input yields an empty store rather than throwing, so a corrupt or
|
|
15
|
+
* blank file degrades to "no memories yet" instead of crashing the server on startup. */
|
|
16
|
+
export function migrate(parsed, policy) {
|
|
17
|
+
// Legacy v1: a bare array of memories. Wrap it and backfill `expiresAt` so volatile
|
|
18
|
+
// records saved before Phase 2 start honoring their type's freshness window.
|
|
19
|
+
if (Array.isArray(parsed)) {
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
22
|
+
memories: parsed.map((m) => backfillExpiry(m, policy)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// v2+: already an envelope. Future migrations (v2→v3, …) branch on schemaVersion here.
|
|
26
|
+
if (parsed &&
|
|
27
|
+
typeof parsed === "object" &&
|
|
28
|
+
Array.isArray(parsed.memories)) {
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
31
|
+
memories: parsed.memories,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { schemaVersion: CURRENT_SCHEMA_VERSION, memories: [] };
|
|
35
|
+
}
|
|
36
|
+
/** Derive `expiresAt` for a legacy record that predates the expiry field. Records that
|
|
37
|
+
* already have one, or that carry no type, are returned untouched. */
|
|
38
|
+
function backfillExpiry(m, policy) {
|
|
39
|
+
if (m.expiresAt !== undefined || m.type === undefined)
|
|
40
|
+
return m;
|
|
41
|
+
const expiresAt = computeExpiresAt(m.type, m.createdAt, policy);
|
|
42
|
+
return expiresAt ? { ...m, expiresAt } : m;
|
|
43
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Type-based TTL / expiry policy (Phase 2, item 2; RULES §2.5, §4).
|
|
2
|
+
//
|
|
3
|
+
// Every memory is a timestamped event with a freshness window, not a standing rule.
|
|
4
|
+
// The window is derived from the memory's `type`, mirroring the 5-layer model in
|
|
5
|
+
// RULES §4 (organized by how fast each layer changes):
|
|
6
|
+
//
|
|
7
|
+
// identity — who the user is (name, role, language) ............ never expires
|
|
8
|
+
// preference — lasting, identity-adjacent trait ................... never expires
|
|
9
|
+
// project — what they're building (weeks–months) .............. long TTL
|
|
10
|
+
// state — volatile focus / physical / emotional state ....... short TTL
|
|
11
|
+
//
|
|
12
|
+
// A memory with no `type` gets no expiry: we do not guess a lifespan for something we
|
|
13
|
+
// could not classify — better to keep it than silently drop it.
|
|
14
|
+
//
|
|
15
|
+
// Every default is overridable via environment variables (see ENV_KEYS below), so a
|
|
16
|
+
// deployment can tune freshness without code changes. This module is pure and holds no
|
|
17
|
+
// I/O, which keeps the policy trivially testable.
|
|
18
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
/** Default freshness windows, in days. `null` = never. Overridable per type via env. */
|
|
20
|
+
export const DEFAULT_TTL_DAYS = {
|
|
21
|
+
identity: null,
|
|
22
|
+
preference: null,
|
|
23
|
+
project: 90,
|
|
24
|
+
state: 2,
|
|
25
|
+
};
|
|
26
|
+
/** How long (days) a soft-expired record is retained before compaction may drop it. */
|
|
27
|
+
export const DEFAULT_COMPACT_GRACE_DAYS = 30;
|
|
28
|
+
/** Env var that overrides each type's TTL. Value: a number of days, or `never`/`none`. */
|
|
29
|
+
const ENV_KEYS = {
|
|
30
|
+
identity: "JAMGATE_TTL_IDENTITY_DAYS",
|
|
31
|
+
preference: "JAMGATE_TTL_PREFERENCE_DAYS",
|
|
32
|
+
project: "JAMGATE_TTL_PROJECT_DAYS",
|
|
33
|
+
state: "JAMGATE_TTL_STATE_DAYS",
|
|
34
|
+
};
|
|
35
|
+
/** Env var that overrides the compaction grace window (in days). */
|
|
36
|
+
export const GRACE_ENV_KEY = "JAMGATE_COMPACT_GRACE_DAYS";
|
|
37
|
+
/** Parse a days value into milliseconds. `never`/`none`/`off` → null; garbage → fallback. */
|
|
38
|
+
function parseDaysMs(raw, fallbackMs) {
|
|
39
|
+
if (raw === undefined)
|
|
40
|
+
return fallbackMs;
|
|
41
|
+
const t = raw.trim().toLowerCase();
|
|
42
|
+
if (t === "never" || t === "none" || t === "off")
|
|
43
|
+
return null;
|
|
44
|
+
const n = Number(t);
|
|
45
|
+
if (!Number.isFinite(n) || n < 0)
|
|
46
|
+
return fallbackMs; // ignore junk, keep the default
|
|
47
|
+
return n * DAY_MS;
|
|
48
|
+
}
|
|
49
|
+
/** Resolve the effective TTL policy, applying any per-type env overrides. */
|
|
50
|
+
export function resolveTtlPolicy(env = process.env) {
|
|
51
|
+
const policy = {};
|
|
52
|
+
for (const type of Object.keys(ENV_KEYS)) {
|
|
53
|
+
const def = DEFAULT_TTL_DAYS[type];
|
|
54
|
+
policy[type] = parseDaysMs(env[ENV_KEYS[type]], def === null ? null : def * DAY_MS);
|
|
55
|
+
}
|
|
56
|
+
return policy;
|
|
57
|
+
}
|
|
58
|
+
/** Resolve the compaction grace window, in milliseconds. Never falls back to null. */
|
|
59
|
+
export function resolveGraceMs(env = process.env) {
|
|
60
|
+
const parsed = parseDaysMs(env[GRACE_ENV_KEY], DEFAULT_COMPACT_GRACE_DAYS * DAY_MS);
|
|
61
|
+
return parsed ?? DEFAULT_COMPACT_GRACE_DAYS * DAY_MS;
|
|
62
|
+
}
|
|
63
|
+
/** Compute the expiry timestamp for a memory of `type` created at `createdAtISO`.
|
|
64
|
+
* Returns undefined for untyped or never-expiring types. */
|
|
65
|
+
export function computeExpiresAt(type, createdAtISO, policy) {
|
|
66
|
+
if (!type)
|
|
67
|
+
return undefined;
|
|
68
|
+
const ttl = policy[type];
|
|
69
|
+
if (ttl === null || ttl === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
return new Date(new Date(createdAtISO).getTime() + ttl).toISOString();
|
|
72
|
+
}
|
|
73
|
+
/** A memory is (soft-)expired once its expiry has passed. No expiry → never expires. */
|
|
74
|
+
export function isExpired(expiresAt, nowMs) {
|
|
75
|
+
if (!expiresAt)
|
|
76
|
+
return false;
|
|
77
|
+
return new Date(expiresAt).getTime() <= nowMs;
|
|
78
|
+
}
|
|
79
|
+
/** A memory is compactable once it has been expired for longer than the grace window.
|
|
80
|
+
* Soft-expired-but-within-grace records are kept (hidden from recall, still auditable). */
|
|
81
|
+
export function isCompactable(expiresAt, nowMs, graceMs) {
|
|
82
|
+
if (!expiresAt)
|
|
83
|
+
return false;
|
|
84
|
+
return new Date(expiresAt).getTime() + graceMs <= nowMs;
|
|
85
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The storage boundary for Jamgate (D-019).
|
|
2
|
+
// The gate and the MCP server depend ONLY on the types and the `MemoryStore` interface
|
|
3
|
+
// here — never on a concrete backend. That keeps storage swappable: the flat-file store
|
|
4
|
+
// today, SQLite or a hosted Supabase store tomorrow, all behind the same contract.
|
|
5
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jamgate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.amirj4m/jamgate",
|
|
5
|
+
"description": "A neutral, cross-agent memory quality gate for AI agents, delivered as an MCP server — a gate, not a store.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"memory",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"quality-gate",
|
|
12
|
+
"claude",
|
|
13
|
+
"cursor",
|
|
14
|
+
"local-first",
|
|
15
|
+
"embeddings",
|
|
16
|
+
"llm"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "jam",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"homepage": "https://github.com/amirj4m/jamgate#readme",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/amirj4m/jamgate.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/amirj4m/jamgate/issues"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"jamgate": "dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"start": "node dist/index.js",
|
|
40
|
+
"dev": "tsc --watch",
|
|
41
|
+
"pretest": "tsc -p tsconfig.test.json",
|
|
42
|
+
"test": "node --test dist-test/test/*.test.js",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@huggingface/transformers": "^3.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@huggingface/transformers": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.0.0",
|
|
61
|
+
"typescript": "^5.5.0"
|
|
62
|
+
}
|
|
63
|
+
}
|