purra-mem0 1.0.0 → 1.0.4
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/README.md +51 -0
- package/README.zh-CN.md +39 -0
- package/dist/context.d.ts +3 -1
- package/dist/context.js +27 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/journal.d.ts +17 -0
- package/dist/journal.js +68 -0
- package/dist/memory.d.ts +48 -2
- package/dist/memory.js +179 -18
- package/dist/workflow.d.ts +2 -0
- package/dist/workflow.js +30 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -133,6 +133,41 @@ pass `policy(candidate, review)` and a stable `policyRevision` to `MemoryWorkflo
|
|
|
133
133
|
Return an authorized `MemoryResolution` that retains the review key, or `undefined`
|
|
134
134
|
to leave the candidate pending. The policy may be async.
|
|
135
135
|
|
|
136
|
+
To require authorization before messages can reach Mem0 or its providers, bind a
|
|
137
|
+
host-issued grant to the exact capture intent:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { MemoryWorkflow, memoryCaptureIntent } from "purra-mem0";
|
|
141
|
+
|
|
142
|
+
const capturePolicyId = "project-memory";
|
|
143
|
+
const capturePolicyRevision = "2026-09-09";
|
|
144
|
+
const authorization = {
|
|
145
|
+
intentDigest: memoryCaptureIntent(messages, { source, metadata }),
|
|
146
|
+
policyId: capturePolicyId,
|
|
147
|
+
policyRevision: capturePolicyRevision,
|
|
148
|
+
principalId: authenticatedUserId,
|
|
149
|
+
decisionId: hostDecisionId,
|
|
150
|
+
};
|
|
151
|
+
const workflow = new MemoryWorkflow(memory, { capturePolicyId, capturePolicyRevision });
|
|
152
|
+
await memory.recordCaptureAuthorization(authorization, {
|
|
153
|
+
key: `capture-authorization:${hostDecisionId}`,
|
|
154
|
+
expiresAt: captureAuthorizationExpiresAt,
|
|
155
|
+
});
|
|
156
|
+
const result = await workflow.capture(messages, {
|
|
157
|
+
source, key: operationKey, metadata, authorization,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The host owns authentication, eligibility and audit of the decision. Keep the
|
|
162
|
+
authorization unchanged on retry. Missing or mismatched grants fail before the
|
|
163
|
+
operation journal and Provider calls; changing a bound grant under the same key
|
|
164
|
+
causes an idempotency conflict.
|
|
165
|
+
|
|
166
|
+
Call `revokeCaptureAuthorization(policyId, decisionId, { key })` to persist a
|
|
167
|
+
revocation. It prevents new extraction, review and activation under the decision
|
|
168
|
+
and hides records produced by it. Expiry only stops new processing. Neither path
|
|
169
|
+
physically deletes Mem0 content or history.
|
|
170
|
+
|
|
136
171
|
## Lifecycle
|
|
137
172
|
|
|
138
173
|
Before shutdown, call `await memory.drain()` and `memory.close()`, then close SDK
|
|
@@ -143,3 +178,19 @@ writes, see the [memory lifecycle guide](../README.md).
|
|
|
143
178
|
## Native managed providers
|
|
144
179
|
|
|
145
180
|
`createManagedClient` inject native LLM and Embedding adapters into a version-pinned, package-private Mem0 module. Calls retain the existing PurrA budget, cancellation and result validation. Explicit vector dimensions must agree; the managed path rejects LangChain vector stores. It neither patches the installed SDK nor starts a proxy server. See [third-party notices](THIRD_PARTY_NOTICES.md). Raw SDK clients remain host-owned and do not acquire managed accounting automatically.
|
|
181
|
+
|
|
182
|
+
## Host-controlled selection and relation proposals
|
|
183
|
+
|
|
184
|
+
`new MemoryContext({ memory, selectIds: hostSelect })` accepts an async
|
|
185
|
+
`hostSelect(request, signal)` returning ordered memory IDs. Supply either
|
|
186
|
+
`selectIds` or the existing `query`, never both. The component rereads authorized
|
|
187
|
+
records, validates evidence, and fits complete records into the context budget.
|
|
188
|
+
The host owns retrieval combination, ordering, callback timeout and model costs.
|
|
189
|
+
|
|
190
|
+
`proposeMemoryRelations(memory, refs, { relations: allowedNames, extract: hostExtract })`
|
|
191
|
+
is exported from `purra-mem0`. Its async extractor receives `(input, signal)` and
|
|
192
|
+
returns objects with exactly `from`, `to`, `relation`, `fromQuote`, `toQuote` string
|
|
193
|
+
fields. IDs must match supplied `MemoryRef` revisions and quotes must occur in
|
|
194
|
+
those records. Returned immutable proposals do not write links or prove that a
|
|
195
|
+
relation is true. The host reviews proposals and explicitly calls `memory.link`.
|
|
196
|
+
No automatic model invocation, retry or adoption policy is installed.
|
package/README.zh-CN.md
CHANGED
|
@@ -123,6 +123,34 @@ async function capture(
|
|
|
123
123
|
`policy(candidate, review)` 和稳定的 `policyRevision`。
|
|
124
124
|
策略返回已授权且保留审查键的 `MemoryResolution`,或返回 `undefined` 保持待处理;策略可以是异步函数。
|
|
125
125
|
|
|
126
|
+
需要在消息到达 Mem0 或 Provider 前强制授权时,绑定宿主签发的准确捕获意图:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { MemoryWorkflow, memoryCaptureIntent } from "purra-mem0";
|
|
130
|
+
|
|
131
|
+
const authorization = {
|
|
132
|
+
intentDigest: memoryCaptureIntent(messages, { source, metadata }),
|
|
133
|
+
policyId: capturePolicyId,
|
|
134
|
+
policyRevision: capturePolicyRevision,
|
|
135
|
+
principalId: authenticatedUserId,
|
|
136
|
+
decisionId: hostDecisionId,
|
|
137
|
+
};
|
|
138
|
+
const workflow = new MemoryWorkflow(memory, { capturePolicyId, capturePolicyRevision });
|
|
139
|
+
await memory.recordCaptureAuthorization(authorization, {
|
|
140
|
+
key: `capture-authorization:${hostDecisionId}`,
|
|
141
|
+
expiresAt: captureAuthorizationExpiresAt,
|
|
142
|
+
});
|
|
143
|
+
const result = await workflow.capture(messages, {
|
|
144
|
+
source, key: operationKey, metadata, authorization,
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
宿主负责认证、内容适用性和决定审计。重试必须沿用相同授权;缺失或不匹配的授权会在
|
|
149
|
+
操作日志及 Provider 调用前失败,同一 key 更换已绑定授权会触发幂等冲突。
|
|
150
|
+
调用 `revokeCaptureAuthorization(policyId, decisionId, { key })` 可持久撤销决定,阻止
|
|
151
|
+
新的提取、审查和激活,并隐藏由其产生的记录。到期只阻止新处理;两者均不物理删除
|
|
152
|
+
Mem0 内容或历史。
|
|
153
|
+
|
|
126
154
|
## 生命周期
|
|
127
155
|
|
|
128
156
|
关闭前调用 `await memory.drain()` 和 `memory.close()`,再关闭 SDK 与模型服务资源。
|
|
@@ -132,3 +160,14 @@ async function capture(
|
|
|
132
160
|
## 原生受控适配
|
|
133
161
|
|
|
134
162
|
`createManagedClient` 把 PurrA 的原生模型和向量适配器注入包内、版本固定的 Mem0 私有模块,沿用现有预算、取消与结果校验。向量维度必须一致,受控路径拒绝 LangChain 向量库。不修改宿主安装的官方 SDK,也不启动代理服务。来源、许可证与修改说明见[第三方声明](THIRD_PARTY_NOTICES.md)。直接传入的原始 SDK 客户端仍由宿主管理,不自动获得受控回调的计费。
|
|
163
|
+
|
|
164
|
+
## 宿主选择与关系候选
|
|
165
|
+
|
|
166
|
+
`MemoryContext` 支持异步 `selectIds(request, signal)`,与原有 `query` 二选一。
|
|
167
|
+
宿主返回有序记忆 ID;组件重新读取并验证来源、版本和预算,不接受替换文本。
|
|
168
|
+
|
|
169
|
+
`purra-mem0` 导出的 `proposeMemoryRelations` 接受记忆引用、宿主关系词表和异步 `extract`。
|
|
170
|
+
抽取回调返回仅含 `from`、`to`、`relation`、`fromQuote`、`toQuote` 字符串的对象数组。
|
|
171
|
+
组件校验原文引用与当前记忆版本,只返回候选,不自动写入关系。
|
|
172
|
+
关系是否成立、是否采纳和显式调用 `memory.link` 均由宿主决定。
|
|
173
|
+
模型调用的授权、超时和费用预算也由宿主负责。
|
package/dist/context.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ContextBlock, ContextEvidenceReceipt, ContextBudget, ContextBudgetClaim, ContextBundle, ContextProvider, ContextRequest } from "purra";
|
|
2
2
|
import { Mem0Memory } from "./memory.js";
|
|
3
|
+
export type MemorySelectionPolicy = (request: ContextRequest, signal?: AbortSignal) => Promise<readonly string[]>;
|
|
3
4
|
export declare class MemoryContext implements ContextProvider {
|
|
4
5
|
#private;
|
|
5
6
|
constructor(options: {
|
|
6
7
|
memory: Mem0Memory;
|
|
7
|
-
query
|
|
8
|
+
query?: (request: ContextRequest) => string;
|
|
9
|
+
selectIds?: MemorySelectionPolicy;
|
|
8
10
|
countTokens?: (content: string) => number;
|
|
9
11
|
name?: string;
|
|
10
12
|
desiredTokens?: number;
|
package/dist/context.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { estimateJsonTokens } from "purra";
|
|
2
2
|
import { Mem0Memory, positiveInteger, requiredText } from "./memory.js";
|
|
3
|
+
import { MemoryError } from "./journal.js";
|
|
4
|
+
function validateContextAllowance(value) {
|
|
5
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 1_000_000)
|
|
6
|
+
throw new TypeError("invalid context allowance");
|
|
7
|
+
}
|
|
3
8
|
export class MemoryContext {
|
|
4
9
|
#memory;
|
|
5
10
|
#query;
|
|
11
|
+
#selectIds;
|
|
6
12
|
#countTokens;
|
|
7
13
|
#name;
|
|
8
14
|
#desiredTokens;
|
|
@@ -10,32 +16,44 @@ export class MemoryContext {
|
|
|
10
16
|
constructor(options) {
|
|
11
17
|
this.#memory = options.memory;
|
|
12
18
|
this.#query = options.query;
|
|
19
|
+
this.#selectIds = options.selectIds;
|
|
13
20
|
this.#countTokens = options.countTokens ?? estimateJsonTokens;
|
|
14
21
|
this.#name = requiredText(options.name ?? "memory", "context name", 128);
|
|
15
22
|
this.#desiredTokens = positiveInteger(options.desiredTokens ?? 1024, "desiredTokens", 1_000_000);
|
|
16
23
|
this.#limit = positiveInteger(options.limit ?? 8, "limit");
|
|
17
|
-
if (
|
|
18
|
-
throw new TypeError("
|
|
24
|
+
if ((this.#query === undefined) === (this.#selectIds === undefined))
|
|
25
|
+
throw new TypeError("Provide exactly one of query or selectIds");
|
|
26
|
+
if (typeof (this.#selectIds ?? this.#query) !== "function" || typeof this.#countTokens !== "function")
|
|
27
|
+
throw new TypeError("Selection and countTokens must be host functions");
|
|
19
28
|
}
|
|
20
29
|
describeContextDemands() {
|
|
21
30
|
return [{ name: this.#name, desiredTokens: this.#desiredTokens }];
|
|
22
31
|
}
|
|
23
32
|
async buildContext(request, budget, signal) {
|
|
24
|
-
const allowance = budget.contextAllocations[this.#name]
|
|
25
|
-
|
|
33
|
+
const allowance = Object.hasOwn(budget.contextAllocations, this.#name) ? budget.contextAllocations[this.#name] : 0;
|
|
34
|
+
validateContextAllowance(allowance);
|
|
35
|
+
if (allowance === 0)
|
|
26
36
|
return { blocks: [] };
|
|
27
37
|
const epoch = this.#memory.epoch;
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
throw new MemoryError("memory_cancelled");
|
|
40
|
+
const ids = this.#selectIds === undefined
|
|
41
|
+
? (await this.#memory.retrieve({ query: this.#query(request), limit: this.#limit, scope: {} }, signal)).map(hit => hit.id)
|
|
42
|
+
: await this.#selectIds(request, signal);
|
|
43
|
+
const result = await assembleMemoryContext(this.#memory, ids, allowance, { name: this.#name, countTokens: this.#countTokens, expectedEpoch: epoch, ...(signal ? { signal } : {}) });
|
|
30
44
|
return { blocks: result.block ? [result.block] : [] };
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
/** Fresh authorized records; shared by Agent and non-Run consumers. */
|
|
34
48
|
export async function assembleMemoryContext(memory, ids, allowance, options = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
validateContextAllowance(allowance);
|
|
50
|
+
const countTokens = options.countTokens === undefined ? estimateJsonTokens : options.countTokens;
|
|
51
|
+
if (typeof countTokens !== "function")
|
|
52
|
+
throw new TypeError("countTokens must be callable");
|
|
37
53
|
const name = requiredText(options.name ?? "memory", "context name", 128);
|
|
38
|
-
|
|
54
|
+
if (!Array.isArray(ids))
|
|
55
|
+
throw new TypeError("ids must be an array");
|
|
56
|
+
ids = Object.freeze([...ids]);
|
|
39
57
|
const epoch = options.expectedEpoch ?? memory.epoch;
|
|
40
58
|
memory.assertEpoch(epoch);
|
|
41
59
|
const hits = await memory.select(ids, options.signal ? { signal: options.signal } : {});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Optional Node.js integration; not imported by PurrA Core. */
|
|
2
2
|
export { MemoryError } from "./journal.js";
|
|
3
|
-
export { Mem0Memory } from "./memory.js";
|
|
4
|
-
export type { Mem0Client, MemoryScope, MemorySource, MemoryRecord, MemoryMetadata, MemoryFilters, MemoryPage, MemoryOperation, MemoryRef, MemoryLink, MemoryLinkPage, MemoryResolution, MemoryReview, MemoryMatch } from "./memory.js";
|
|
5
|
-
export type { MemoryContextResult } from "./context.js";
|
|
3
|
+
export { Mem0Memory, memoryCaptureIntent } from "./memory.js";
|
|
4
|
+
export type { Mem0Client, MemoryScope, MemorySource, MemoryCaptureAuthorization, MemoryRecord, MemoryMetadata, MemoryFilters, MemoryPage, MemoryOperation, MemoryRef, MemoryLink, MemoryLinkPage, MemoryRelationEvidence, MemoryResolution, MemoryReview, MemoryMatch } from "./memory.js";
|
|
5
|
+
export type { MemoryContextResult, MemorySelectionPolicy } from "./context.js";
|
|
6
6
|
export { MemoryContext, assembleMemoryContext } from "./context.js";
|
|
7
7
|
export { createManagedClient, runModel } from "./providers.js";
|
|
8
8
|
export type { MemoryBudget, MemoryProviders, MemoryUsage, EmbeddingResult, ManagedMem0Config } from "./providers.js";
|
|
9
9
|
export { MemoryWorkflow } from "./workflow.js";
|
|
10
10
|
export type { MemoryWorkflowResult, MemoryDecisionPolicy } from "./workflow.js";
|
|
11
|
+
export { proposeMemoryRelations } from './relations.js';
|
|
12
|
+
export type { MemoryRelationProposal, MemoryRelationExtractionInput, MemoryRelationExtractor } from './relations.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Optional Node.js integration; not imported by PurrA Core. */
|
|
2
2
|
export { MemoryError } from "./journal.js";
|
|
3
|
-
export { Mem0Memory } from "./memory.js";
|
|
3
|
+
export { Mem0Memory, memoryCaptureIntent } from "./memory.js";
|
|
4
4
|
export { MemoryContext, assembleMemoryContext } from "./context.js";
|
|
5
5
|
export { createManagedClient, runModel } from "./providers.js";
|
|
6
6
|
export { MemoryWorkflow } from "./workflow.js";
|
|
7
|
+
export { proposeMemoryRelations } from './relations.js';
|
package/dist/journal.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface Metadata {
|
|
|
18
18
|
purra_reason: string | null;
|
|
19
19
|
purra_created: string;
|
|
20
20
|
purra_updated: string;
|
|
21
|
+
purra_capture_policy?: string;
|
|
22
|
+
purra_capture_decision?: string;
|
|
21
23
|
}
|
|
22
24
|
export interface ItemView {
|
|
23
25
|
version: number;
|
|
@@ -53,6 +55,17 @@ export interface Plan {
|
|
|
53
55
|
policy_hash?: string;
|
|
54
56
|
changes?: Partial<ItemView>;
|
|
55
57
|
link?: Omit<MemoryLink, "key" | "valid">;
|
|
58
|
+
capture_authorization?: {
|
|
59
|
+
intent_digest: string;
|
|
60
|
+
policy_id: string;
|
|
61
|
+
policy_revision: string;
|
|
62
|
+
principal_id: string;
|
|
63
|
+
decision_id: string;
|
|
64
|
+
};
|
|
65
|
+
valid_from?: string | null;
|
|
66
|
+
expires?: string | null;
|
|
67
|
+
policy_id?: string;
|
|
68
|
+
decision_id?: string;
|
|
56
69
|
}
|
|
57
70
|
export interface OperationRow {
|
|
58
71
|
key: string;
|
|
@@ -83,6 +96,10 @@ export declare class Journal {
|
|
|
83
96
|
revoked(source: string, revision: string): boolean;
|
|
84
97
|
assertSource(meta: Pick<Metadata, "purra_source" | "purra_revision">): void;
|
|
85
98
|
revokeSource(key: string, fingerprint: string, source: string, revision: string | null): void;
|
|
99
|
+
captureRevoked(policy: string, decision: string): boolean;
|
|
100
|
+
assertCaptureAuthorization(authorization: NonNullable<Plan["capture_authorization"]>): void;
|
|
101
|
+
recordCaptureAuthorization(key: string, fingerprint: string, authorization: NonNullable<Plan["capture_authorization"]>, validFrom: string | null, expires: string | null): void;
|
|
102
|
+
revokeCaptureAuthorization(key: string, fingerprint: string, policy: string, decision: string): void;
|
|
86
103
|
item(id: string): Item | undefined;
|
|
87
104
|
resolve(key: string, fingerprint: string, plan: Plan, epoch: number): void;
|
|
88
105
|
control(key: string, fingerprint: string, plan: Plan, epoch: number, refs: readonly MemoryRef[], changes: readonly Partial<ItemView>[]): void;
|
package/dist/journal.js
CHANGED
|
@@ -47,6 +47,13 @@ export class Journal {
|
|
|
47
47
|
CREATE TABLE IF NOT EXISTS purra_mem0_revocations (
|
|
48
48
|
scope TEXT NOT NULL, source TEXT NOT NULL, revision TEXT NOT NULL,
|
|
49
49
|
PRIMARY KEY(scope,source,revision));
|
|
50
|
+
CREATE TABLE IF NOT EXISTS purra_mem0_capture_authorizations (
|
|
51
|
+
scope TEXT NOT NULL, policy TEXT NOT NULL, decision TEXT NOT NULL,
|
|
52
|
+
authorization TEXT NOT NULL, valid_from TEXT, expires TEXT,
|
|
53
|
+
PRIMARY KEY(scope,policy,decision));
|
|
54
|
+
CREATE TABLE IF NOT EXISTS purra_mem0_capture_revocations (
|
|
55
|
+
scope TEXT NOT NULL, policy TEXT NOT NULL, decision TEXT NOT NULL,
|
|
56
|
+
PRIMARY KEY(scope,policy,decision));
|
|
50
57
|
`);
|
|
51
58
|
this.transaction(() => {
|
|
52
59
|
this.db.prepare("INSERT OR IGNORE INTO purra_mem0_info VALUES ('store',?)").run(randomUUID().replaceAll("-", ""));
|
|
@@ -127,6 +134,8 @@ export class Journal {
|
|
|
127
134
|
this.assertSnapshot(op.plan);
|
|
128
135
|
if (op && op.plan.kind !== "delete" && op.plan.meta)
|
|
129
136
|
this.assertSource(op.plan.meta);
|
|
137
|
+
if (op?.plan.capture_authorization)
|
|
138
|
+
this.assertCaptureAuthorization(op.plan.capture_authorization);
|
|
130
139
|
const limits = JSON.parse(this.db.prepare("SELECT limits FROM purra_mem0_budgets WHERE scope=? AND key=?").get(this.scope, budget).limits);
|
|
131
140
|
const used = this.usage("budget", budget);
|
|
132
141
|
if (used[kind === "llm" ? "llmCalls" : "embeddingCalls"] + 1 > limits["max_" + kind + "_calls"]
|
|
@@ -187,6 +196,63 @@ export class Journal {
|
|
|
187
196
|
}
|
|
188
197
|
});
|
|
189
198
|
}
|
|
199
|
+
captureRevoked(policy, decision) {
|
|
200
|
+
return !!this.db.prepare("SELECT 1 FROM purra_mem0_capture_revocations WHERE scope=? AND policy=? AND decision=?")
|
|
201
|
+
.get(this.scope, policy, decision);
|
|
202
|
+
}
|
|
203
|
+
assertCaptureAuthorization(authorization) {
|
|
204
|
+
if (this.captureRevoked(authorization.policy_id, authorization.decision_id))
|
|
205
|
+
throw new MemoryError("memory_capture_authorization_revoked");
|
|
206
|
+
const row = this.db.prepare("SELECT authorization,valid_from,expires FROM purra_mem0_capture_authorizations WHERE scope=? AND policy=? AND decision=?")
|
|
207
|
+
.get(this.scope, authorization.policy_id, authorization.decision_id);
|
|
208
|
+
if (!row || JSON.stringify(JSON.parse(row.authorization)) !== JSON.stringify(authorization)) {
|
|
209
|
+
throw new MemoryError("memory_capture_authorization_unavailable");
|
|
210
|
+
}
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
if (row.valid_from !== null && Date.parse(row.valid_from) > now)
|
|
213
|
+
throw new MemoryError("memory_capture_authorization_not_yet_valid");
|
|
214
|
+
if (row.expires !== null && Date.parse(row.expires) <= now)
|
|
215
|
+
throw new MemoryError("memory_capture_authorization_expired");
|
|
216
|
+
}
|
|
217
|
+
recordCaptureAuthorization(key, fingerprint, authorization, validFrom, expires) {
|
|
218
|
+
this.transaction(() => {
|
|
219
|
+
const previous = this.operation(key);
|
|
220
|
+
if (previous) {
|
|
221
|
+
if (previous.fingerprint !== fingerprint)
|
|
222
|
+
throw new MemoryError("memory_idempotency_conflict");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (this.captureRevoked(authorization.policy_id, authorization.decision_id))
|
|
226
|
+
throw new MemoryError("memory_capture_authorization_revoked");
|
|
227
|
+
const row = this.db.prepare("SELECT authorization,valid_from,expires FROM purra_mem0_capture_authorizations WHERE scope=? AND policy=? AND decision=?")
|
|
228
|
+
.get(this.scope, authorization.policy_id, authorization.decision_id);
|
|
229
|
+
if (row && (JSON.stringify(JSON.parse(row.authorization)) !== JSON.stringify(authorization)
|
|
230
|
+
|| row.valid_from !== validFrom || row.expires !== expires))
|
|
231
|
+
throw new MemoryError("memory_capture_authorization_conflict");
|
|
232
|
+
const plan = { kind: "record_capture_authorization", target: null, meta: null,
|
|
233
|
+
capture_authorization: authorization, valid_from: validFrom, expires };
|
|
234
|
+
this.db.prepare("INSERT INTO purra_mem0_ops VALUES (?,?,?,'complete',?,'[]')").run(this.scope, key, fingerprint, JSON.stringify(plan));
|
|
235
|
+
if (!row)
|
|
236
|
+
this.db.prepare("INSERT INTO purra_mem0_capture_authorizations VALUES (?,?,?,?,?,?)")
|
|
237
|
+
.run(this.scope, authorization.policy_id, authorization.decision_id, JSON.stringify(authorization), validFrom, expires);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
revokeCaptureAuthorization(key, fingerprint, policy, decision) {
|
|
241
|
+
this.transaction(() => {
|
|
242
|
+
const previous = this.operation(key);
|
|
243
|
+
if (previous) {
|
|
244
|
+
if (previous.fingerprint !== fingerprint)
|
|
245
|
+
throw new MemoryError("memory_idempotency_conflict");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const plan = { kind: "revoke_capture_authorization", target: null, meta: null, policy_id: policy, decision_id: decision };
|
|
249
|
+
this.db.prepare("INSERT INTO purra_mem0_ops VALUES (?,?,?,'complete',?,'[]')").run(this.scope, key, fingerprint, JSON.stringify(plan));
|
|
250
|
+
if (!this.captureRevoked(policy, decision)) {
|
|
251
|
+
this.db.prepare("INSERT INTO purra_mem0_capture_revocations VALUES (?,?,?)").run(this.scope, policy, decision);
|
|
252
|
+
this.db.prepare("UPDATE purra_mem0_epochs SET epoch=epoch+1 WHERE scope=?").run(this.scope);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
190
256
|
item(id) {
|
|
191
257
|
const row = this.db.prepare("SELECT record FROM purra_mem0_items WHERE scope=? AND id=?").get(this.scope, id);
|
|
192
258
|
return row ? JSON.parse(row.record) : undefined;
|
|
@@ -212,6 +278,8 @@ export class Journal {
|
|
|
212
278
|
throw new MemoryError("memory_context_stale");
|
|
213
279
|
if (plan.review_key !== undefined)
|
|
214
280
|
this.assertSnapshot(this.reviewPlan(plan.review_key));
|
|
281
|
+
if (plan.capture_authorization !== undefined)
|
|
282
|
+
this.assertCaptureAuthorization(plan.capture_authorization);
|
|
215
283
|
const records = [], now = Date.now();
|
|
216
284
|
for (const [index, ref] of refs.entries()) {
|
|
217
285
|
const row = this.item(ref.id);
|
package/dist/memory.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export interface MemorySource {
|
|
|
36
36
|
readonly id: string;
|
|
37
37
|
readonly revision: string;
|
|
38
38
|
}
|
|
39
|
+
export interface MemoryCaptureAuthorization {
|
|
40
|
+
readonly intentDigest: string;
|
|
41
|
+
readonly policyId: string;
|
|
42
|
+
readonly policyRevision: string;
|
|
43
|
+
readonly principalId: string;
|
|
44
|
+
readonly decisionId: string;
|
|
45
|
+
}
|
|
39
46
|
export type MemoryMetadata = Readonly<Record<string, string | number | boolean | null>>;
|
|
40
47
|
export type MemoryFilters = Readonly<Record<string, string | number | boolean | null | readonly (string | number | boolean | null)[]>>;
|
|
41
48
|
export interface MemoryRecord {
|
|
@@ -74,6 +81,15 @@ export interface MemoryLinkPage {
|
|
|
74
81
|
readonly next: string | null;
|
|
75
82
|
readonly epoch: number;
|
|
76
83
|
}
|
|
84
|
+
export interface MemoryRelationEvidence {
|
|
85
|
+
readonly evidenceId: string;
|
|
86
|
+
readonly source: string;
|
|
87
|
+
readonly linkKey: string;
|
|
88
|
+
readonly from: MemoryRef;
|
|
89
|
+
readonly to: MemoryRef;
|
|
90
|
+
readonly relation: string;
|
|
91
|
+
readonly note: string;
|
|
92
|
+
}
|
|
77
93
|
export type MemoryResolution = {
|
|
78
94
|
readonly items: readonly MemoryRef[];
|
|
79
95
|
readonly reviewKey?: string;
|
|
@@ -112,11 +128,23 @@ interface SourceOptions extends WriteOptions {
|
|
|
112
128
|
readonly expiresAt?: string | null;
|
|
113
129
|
readonly metadata?: MemoryMetadata;
|
|
114
130
|
}
|
|
131
|
+
interface ExtractOptions extends SourceOptions {
|
|
132
|
+
readonly authorization?: MemoryCaptureAuthorization;
|
|
133
|
+
}
|
|
115
134
|
interface VersionOptions extends WriteOptions {
|
|
116
135
|
readonly version: number;
|
|
117
136
|
}
|
|
118
137
|
export declare function requiredText(value: unknown, label: string, limit?: number): string;
|
|
119
138
|
export declare function positiveInteger(value: number, label: string, maximum?: number): number;
|
|
139
|
+
export declare function memoryCaptureIntent(messages: readonly {
|
|
140
|
+
role: "user" | "assistant";
|
|
141
|
+
content: string;
|
|
142
|
+
}[], options: {
|
|
143
|
+
source: MemorySource;
|
|
144
|
+
metadata?: MemoryMetadata;
|
|
145
|
+
expiresAt?: string | null;
|
|
146
|
+
maxInputChars?: number;
|
|
147
|
+
}): string;
|
|
120
148
|
/** Host-owned SDK, immutable scope, persistent write fence. No automatic capture. */
|
|
121
149
|
export declare class Mem0Memory implements Retriever {
|
|
122
150
|
#private;
|
|
@@ -130,9 +158,17 @@ export declare class Mem0Memory implements Retriever {
|
|
|
130
158
|
maxInputChars?: number;
|
|
131
159
|
providers?: MemoryProviders;
|
|
132
160
|
});
|
|
161
|
+
get maxInputChars(): number;
|
|
133
162
|
operation(key: string): MemoryOperation | undefined;
|
|
163
|
+
recordCaptureAuthorization(authorizationValue: MemoryCaptureAuthorization, options: WriteOptions & {
|
|
164
|
+
validFrom?: string | null;
|
|
165
|
+
expiresAt?: string | null;
|
|
166
|
+
}): Promise<MemoryOperation>;
|
|
167
|
+
revokeCaptureAuthorization(policyIdValue: string, decisionIdValue: string, options: WriteOptions): Promise<MemoryOperation>;
|
|
134
168
|
/** Verify content, then atomically keep one claim or quarantine a group. No SDK mutations. */
|
|
135
|
-
resolve(value: MemoryResolution, options: WriteOptions
|
|
169
|
+
resolve(value: MemoryResolution, options: WriteOptions & {
|
|
170
|
+
authorization?: MemoryCaptureAuthorization;
|
|
171
|
+
}): Promise<MemoryOperation>;
|
|
136
172
|
/** Budgeted advice for a pending candidate; never activates memory. */
|
|
137
173
|
link(from: MemoryRef, to: MemoryRef, relation: string, options: WriteOptions & {
|
|
138
174
|
note?: string;
|
|
@@ -140,11 +176,21 @@ export declare class Mem0Memory implements Retriever {
|
|
|
140
176
|
links(id: string, options?: {
|
|
141
177
|
limit?: number;
|
|
142
178
|
after?: string;
|
|
179
|
+
direction?: "both" | "incoming" | "outgoing";
|
|
180
|
+
relation?: string;
|
|
181
|
+
validOnly?: boolean;
|
|
143
182
|
signal?: AbortSignal;
|
|
144
183
|
}): Promise<MemoryLinkPage>;
|
|
184
|
+
relationEvidence(link: MemoryLink, options?: {
|
|
185
|
+
signal?: AbortSignal;
|
|
186
|
+
}): Promise<MemoryRelationEvidence>;
|
|
187
|
+
validateRelationEvidence(receipts: readonly MemoryRelationEvidence[], options?: {
|
|
188
|
+
signal?: AbortSignal;
|
|
189
|
+
}): Promise<void>;
|
|
145
190
|
review(candidate: MemoryRef, options: WriteOptions & {
|
|
146
191
|
limit?: number;
|
|
147
192
|
instructions?: string;
|
|
193
|
+
authorization?: MemoryCaptureAuthorization;
|
|
148
194
|
}): Promise<MemoryOperation>;
|
|
149
195
|
/** Includes searches, reservations, late completions and unknown usage. */
|
|
150
196
|
budgetUsage(): MemoryUsage | undefined;
|
|
@@ -185,7 +231,7 @@ export declare class Mem0Memory implements Retriever {
|
|
|
185
231
|
extract(messages: readonly {
|
|
186
232
|
role: "user" | "assistant";
|
|
187
233
|
content: string;
|
|
188
|
-
}[], options:
|
|
234
|
+
}[], options: ExtractOptions): Promise<MemoryOperation>;
|
|
189
235
|
update(id: string, text: string, options: SourceOptions & VersionOptions): Promise<MemoryOperation>;
|
|
190
236
|
setState(id: string, state: MemoryRecord["state"], options: VersionOptions & {
|
|
191
237
|
reason?: string | null;
|
package/dist/memory.js
CHANGED
|
@@ -74,6 +74,35 @@ function expiry(value) {
|
|
|
74
74
|
function sourceCopy(source) {
|
|
75
75
|
return Object.freeze({ id: requiredText(source?.id, "source id", 1024), revision: requiredText(source?.revision, "source revision", 512) });
|
|
76
76
|
}
|
|
77
|
+
function captureInput(messages, source, metadata, expiresAt, maxInput) {
|
|
78
|
+
const copiedSource = sourceCopy(source);
|
|
79
|
+
if (!Array.isArray(messages) || messages.length < 1 || messages.length > 100)
|
|
80
|
+
throw new TypeError("messages must contain 1 to 100 source messages");
|
|
81
|
+
const copied = messages.map(message => {
|
|
82
|
+
if (!message || Object.keys(message).sort().join(",") !== "content,role" || !["user", "assistant"].includes(message.role)) {
|
|
83
|
+
throw new TypeError("source messages may contain only user/assistant text");
|
|
84
|
+
}
|
|
85
|
+
return { role: message.role, content: requiredText(message.content, "message", maxInput) };
|
|
86
|
+
});
|
|
87
|
+
if (copied.reduce((total, message) => total + [...message.content].length, 0) > maxInput)
|
|
88
|
+
throw new TypeError("source messages exceed maxInputChars");
|
|
89
|
+
return { messages: copied, source: copiedSource, metadata: metadataCopy(metadata ?? {}), expiresAt: expiry(expiresAt) };
|
|
90
|
+
}
|
|
91
|
+
export function memoryCaptureIntent(messages, options) {
|
|
92
|
+
const maxInput = positiveInteger(options.maxInputChars ?? 32_000, "maxInputChars", 1_000_000);
|
|
93
|
+
const input = captureInput(messages, options.source, options.metadata, options.expiresAt, maxInput);
|
|
94
|
+
return "sha256:" + digest(["purra.mem0.capture-intent/v1", input.messages,
|
|
95
|
+
[input.source.id, input.source.revision], input.metadata, input.expiresAt]);
|
|
96
|
+
}
|
|
97
|
+
function authorizationCopy(value) {
|
|
98
|
+
if (!value || typeof value !== "object" || !/^sha256:[0-9a-f]{64}$/.test(value.intentDigest))
|
|
99
|
+
throw new TypeError("invalid capture authorization");
|
|
100
|
+
return Object.freeze({ intentDigest: value.intentDigest,
|
|
101
|
+
policyId: requiredText(value.policyId, "capture policy id", 512),
|
|
102
|
+
policyRevision: requiredText(value.policyRevision, "capture policy revision", 512),
|
|
103
|
+
principalId: requiredText(value.principalId, "capture principal id", 512),
|
|
104
|
+
decisionId: requiredText(value.decisionId, "capture decision id", 512) });
|
|
105
|
+
}
|
|
77
106
|
function resolutionCopy(value, limit = 100) {
|
|
78
107
|
if (!value || !["independent", "duplicate", "supersede", "conflict"].includes(value.kind) || !Array.isArray(value.items)
|
|
79
108
|
|| value.items.length > limit || (value.kind === "independent" ? value.items.length !== 1 : value.items.length < 2)) {
|
|
@@ -154,6 +183,7 @@ export class Mem0Memory {
|
|
|
154
183
|
throw error;
|
|
155
184
|
}
|
|
156
185
|
}
|
|
186
|
+
get maxInputChars() { return this.#maxInput; }
|
|
157
187
|
async #call(work, signal) {
|
|
158
188
|
if (this.#closed)
|
|
159
189
|
throw new MemoryError("memory_closed");
|
|
@@ -189,21 +219,47 @@ export class Mem0Memory {
|
|
|
189
219
|
operation(key) {
|
|
190
220
|
const op = this.#journal.operation(requiredText(key, "operation key", 512));
|
|
191
221
|
return op ? Object.freeze({ key, state: op.state, ids: Object.freeze(op.ids ?? []),
|
|
192
|
-
usage: op.plan.budget || ["revoke_source", "state", "annotate", "resolve", "link"].includes(op.plan.kind) ? this.#journal.usage("operation", key) : "unknown",
|
|
222
|
+
usage: op.plan.budget || ["revoke_source", "record_capture_authorization", "revoke_capture_authorization", "state", "annotate", "resolve", "link"].includes(op.plan.kind) ? this.#journal.usage("operation", key) : "unknown",
|
|
193
223
|
...(op.plan.resolution ? { resolution: resolutionCopy(op.plan.resolution) } : {}),
|
|
194
224
|
...(op.plan.review ? { review: reviewCopy(op.plan.review, key) } : {}) }) : undefined;
|
|
195
225
|
}
|
|
226
|
+
async recordCaptureAuthorization(authorizationValue, options) {
|
|
227
|
+
const authorization = authorizationCopy(authorizationValue), key = requiredText(options.key, "operation key", 512);
|
|
228
|
+
const validFrom = expiry(options.validFrom), expires = expiry(options.expiresAt);
|
|
229
|
+
if (validFrom !== null && expires !== null && validFrom >= expires)
|
|
230
|
+
throw new TypeError("capture authorization expiry must follow validFrom");
|
|
231
|
+
const data = { intent_digest: authorization.intentDigest, policy_id: authorization.policyId,
|
|
232
|
+
policy_revision: authorization.policyRevision, principal_id: authorization.principalId, decision_id: authorization.decisionId };
|
|
233
|
+
const fingerprint = digest(["record_capture_authorization", data, validFrom, expires]);
|
|
234
|
+
return this.#call(async () => { this.#journal.recordCaptureAuthorization(key, fingerprint, data, validFrom, expires); return this.operation(key); }, options.signal);
|
|
235
|
+
}
|
|
236
|
+
async revokeCaptureAuthorization(policyIdValue, decisionIdValue, options) {
|
|
237
|
+
const policyId = requiredText(policyIdValue, "capture policy id", 512);
|
|
238
|
+
const decisionId = requiredText(decisionIdValue, "capture decision id", 512);
|
|
239
|
+
const key = requiredText(options.key, "operation key", 512);
|
|
240
|
+
const fingerprint = digest(["revoke_capture_authorization", policyId, decisionId]);
|
|
241
|
+
return this.#call(async () => { this.#journal.revokeCaptureAuthorization(key, fingerprint, policyId, decisionId); return this.operation(key); }, options.signal);
|
|
242
|
+
}
|
|
196
243
|
/** Verify content, then atomically keep one claim or quarantine a group. No SDK mutations. */
|
|
197
244
|
async resolve(value, options) {
|
|
198
245
|
const resolution = resolutionCopy(value, this.#maxResults), key = requiredText(options.key, "operation key", 512);
|
|
199
246
|
const reviewKey = resolution.reviewKey;
|
|
200
247
|
const data = { kind: resolution.kind, items: resolution.items, keep: resolution.keep ?? null };
|
|
201
|
-
const
|
|
248
|
+
const authorization = options.authorization === undefined ? undefined : authorizationCopy(options.authorization);
|
|
249
|
+
const authorizationData = authorization === undefined ? undefined : { intent_digest: authorization.intentDigest,
|
|
250
|
+
policy_id: authorization.policyId, policy_revision: authorization.policyRevision,
|
|
251
|
+
principal_id: authorization.principalId, decision_id: authorization.decisionId };
|
|
252
|
+
const fingerprintInput = reviewKey === undefined ? ["resolve", data] : ["resolve", data, reviewKey];
|
|
253
|
+
if (authorizationData !== undefined)
|
|
254
|
+
fingerprintInput.push(authorizationData);
|
|
255
|
+
const fingerprint = digest(fingerprintInput);
|
|
202
256
|
const plan = { kind: "resolve", target: null, meta: null, resolution };
|
|
203
257
|
if (reviewKey !== undefined)
|
|
204
258
|
plan.review_key = reviewKey;
|
|
205
259
|
if (this.#providers)
|
|
206
260
|
plan.budget = this.#providers.budget.key;
|
|
261
|
+
if (authorizationData !== undefined)
|
|
262
|
+
plan.capture_authorization = authorizationData;
|
|
207
263
|
return this.#call(async () => {
|
|
208
264
|
const previous = this.#journal.operation(key);
|
|
209
265
|
if (previous) {
|
|
@@ -211,6 +267,8 @@ export class Mem0Memory {
|
|
|
211
267
|
throw new MemoryError("memory_idempotency_conflict");
|
|
212
268
|
return this.operation(key);
|
|
213
269
|
}
|
|
270
|
+
if (authorizationData !== undefined)
|
|
271
|
+
this.#journal.assertCaptureAuthorization(authorizationData);
|
|
214
272
|
const epoch = this.epoch;
|
|
215
273
|
let refs = resolution.items;
|
|
216
274
|
if (reviewKey !== undefined) {
|
|
@@ -277,17 +335,64 @@ export class Mem0Memory {
|
|
|
277
335
|
requiredText(id, "memory id", 512);
|
|
278
336
|
const limit = positiveInteger(options.limit ?? 20, "limit", this.#maxResults);
|
|
279
337
|
const after = options.after === undefined ? undefined : requiredText(options.after, "cursor", 512);
|
|
338
|
+
const direction = options.direction === undefined ? "both" : options.direction;
|
|
339
|
+
if (!["both", "incoming", "outgoing"].includes(direction))
|
|
340
|
+
throw new TypeError("Invalid relation direction");
|
|
341
|
+
const relation = options.relation === undefined ? undefined : requiredText(options.relation, "relation", 64);
|
|
342
|
+
const validOnly = options.validOnly === undefined ? false : options.validOnly;
|
|
343
|
+
if (typeof validOnly !== "boolean")
|
|
344
|
+
throw new TypeError("validOnly must be boolean");
|
|
280
345
|
return this.#call(async () => {
|
|
281
346
|
const epoch = this.epoch, rows = this.#journal.links(id, after, limit + 1), items = [];
|
|
282
347
|
for (const { key, data } of rows.slice(0, limit)) {
|
|
348
|
+
if (relation !== undefined && data.relation !== relation
|
|
349
|
+
|| direction === "incoming" && data.to.id !== id
|
|
350
|
+
|| direction === "outgoing" && data.from.id !== id)
|
|
351
|
+
continue;
|
|
283
352
|
const from = await this.#read(data.from.id), to = await this.#read(data.to.id);
|
|
284
353
|
const valid = from !== undefined && to !== undefined && from.version === data.from.version && to.version === data.to.version;
|
|
354
|
+
if (validOnly && !valid)
|
|
355
|
+
continue;
|
|
285
356
|
items.push(Object.freeze({ ...data, from: Object.freeze(data.from), to: Object.freeze(data.to), key, valid }));
|
|
286
357
|
}
|
|
287
358
|
this.assertEpoch(epoch);
|
|
288
359
|
return Object.freeze({ items: Object.freeze(items), next: rows.length > limit ? rows[limit - 1].key : null, epoch });
|
|
289
360
|
}, options.signal);
|
|
290
361
|
}
|
|
362
|
+
async relationEvidence(link, options = {}) {
|
|
363
|
+
if (!link?.valid)
|
|
364
|
+
throw new MemoryError("memory_relation_stale");
|
|
365
|
+
const expected = { from: { id: requiredText(link.from.id, "from id", 512), version: positiveInteger(link.from.version, "from version", 2 ** 31 - 2) },
|
|
366
|
+
to: { id: requiredText(link.to.id, "to id", 512), version: positiveInteger(link.to.version, "to version", 2 ** 31 - 2) },
|
|
367
|
+
relation: requiredText(link.relation, "relation", 64), note: typeof link.note === "string" ? link.note : "" };
|
|
368
|
+
const key = requiredText(link.key, "link key", 512);
|
|
369
|
+
return this.#call(async () => {
|
|
370
|
+
const epoch = this.epoch;
|
|
371
|
+
const operation = this.#journal.operation(key);
|
|
372
|
+
if (!operation || operation.state !== "complete" || operation.plan.kind !== "link"
|
|
373
|
+
|| !isDeepStrictEqual(operation.plan.link, expected))
|
|
374
|
+
throw new MemoryError("memory_relation_stale");
|
|
375
|
+
const [from, to] = await Promise.all([this.#read(expected.from.id), this.#read(expected.to.id)]);
|
|
376
|
+
if (!from || !to || from.version !== expected.from.version || to.version !== expected.to.version)
|
|
377
|
+
throw new MemoryError("memory_relation_stale");
|
|
378
|
+
this.assertEpoch(epoch);
|
|
379
|
+
return Object.freeze({ evidenceId: `mem0-relation:${this.#journal.store}:${digest([key, expected])}`,
|
|
380
|
+
source: "mem0-relation/" + this.#scope, linkKey: key, from: Object.freeze(expected.from),
|
|
381
|
+
to: Object.freeze(expected.to), relation: expected.relation, note: expected.note });
|
|
382
|
+
}, options.signal);
|
|
383
|
+
}
|
|
384
|
+
async validateRelationEvidence(receipts, options = {}) {
|
|
385
|
+
if (!Array.isArray(receipts) || receipts.length > this.#maxResults)
|
|
386
|
+
throw new TypeError("relation receipts must be a bounded array");
|
|
387
|
+
for (const receipt of receipts) {
|
|
388
|
+
if (receipt?.source !== "mem0-relation/" + this.#scope)
|
|
389
|
+
throw new MemoryError("memory_relation_stale");
|
|
390
|
+
const link = Object.freeze({ key: receipt.linkKey, from: receipt.from, to: receipt.to,
|
|
391
|
+
relation: receipt.relation, note: receipt.note, valid: true });
|
|
392
|
+
if (!isDeepStrictEqual(await this.relationEvidence(link, options), receipt))
|
|
393
|
+
throw new MemoryError("memory_relation_stale");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
291
396
|
async review(candidate, options) {
|
|
292
397
|
if (!this.#providers)
|
|
293
398
|
throw new MemoryError("memory_review_requires_managed");
|
|
@@ -297,10 +402,29 @@ export class Mem0Memory {
|
|
|
297
402
|
if (typeof instructions !== "string" || [...instructions].length > 4000)
|
|
298
403
|
throw new TypeError("instructions must be bounded host policy");
|
|
299
404
|
const policy = REVIEW_PROMPT + (instructions ? "\nHost policy:\n" + instructions : "");
|
|
300
|
-
const
|
|
405
|
+
const authorization = options.authorization === undefined ? undefined : authorizationCopy(options.authorization);
|
|
406
|
+
const authorizationData = authorization === undefined ? undefined : { intent_digest: authorization.intentDigest,
|
|
407
|
+
policy_id: authorization.policyId, policy_revision: authorization.policyRevision,
|
|
408
|
+
principal_id: authorization.principalId, decision_id: authorization.decisionId };
|
|
409
|
+
const fingerprintInput = ["review", ref, limit, policy];
|
|
410
|
+
if (authorizationData !== undefined)
|
|
411
|
+
fingerprintInput.push(authorizationData);
|
|
412
|
+
const fingerprint = digest(fingerprintInput);
|
|
301
413
|
const plan = { kind: "review", target: null, meta: null, budget: this.#providers.budget.key, policy_hash: digest(policy) };
|
|
414
|
+
if (authorizationData !== undefined)
|
|
415
|
+
plan.capture_authorization = authorizationData;
|
|
302
416
|
return this.#call(async () => {
|
|
303
|
-
|
|
417
|
+
let previous = this.#journal.operation(key);
|
|
418
|
+
if (previous) {
|
|
419
|
+
if (previous.fingerprint !== fingerprint)
|
|
420
|
+
throw new MemoryError("memory_idempotency_conflict");
|
|
421
|
+
if (previous.state !== "complete")
|
|
422
|
+
throw new MemoryError("memory_operation_unresolved");
|
|
423
|
+
return this.operation(key);
|
|
424
|
+
}
|
|
425
|
+
if (authorizationData !== undefined)
|
|
426
|
+
this.#journal.assertCaptureAuthorization(authorizationData);
|
|
427
|
+
previous = this.#journal.begin(key, fingerprint, plan);
|
|
304
428
|
if (previous) {
|
|
305
429
|
if (previous.state !== "complete")
|
|
306
430
|
throw new MemoryError("memory_operation_unresolved");
|
|
@@ -436,6 +560,9 @@ export class Mem0Memory {
|
|
|
436
560
|
const meta = row.meta;
|
|
437
561
|
if (!internal && this.#journal.revoked(meta.purra_source, meta.purra_revision))
|
|
438
562
|
return undefined;
|
|
563
|
+
if (!internal && meta.purra_capture_policy !== undefined
|
|
564
|
+
&& this.#journal.captureRevoked(meta.purra_capture_policy, meta.purra_capture_decision))
|
|
565
|
+
return undefined;
|
|
439
566
|
if (!internal && this.#journal.writing(id))
|
|
440
567
|
throw new MemoryError("memory_write_busy");
|
|
441
568
|
const result = await this.#client.get(id);
|
|
@@ -446,6 +573,9 @@ export class Mem0Memory {
|
|
|
446
573
|
throw new MemoryError("memory_write_busy");
|
|
447
574
|
if (!internal && this.#journal.revoked(meta.purra_source, meta.purra_revision))
|
|
448
575
|
return undefined;
|
|
576
|
+
if (!internal && meta.purra_capture_policy !== undefined
|
|
577
|
+
&& this.#journal.captureRevoked(meta.purra_capture_policy, meta.purra_capture_decision))
|
|
578
|
+
return undefined;
|
|
449
579
|
const view = itemView(row);
|
|
450
580
|
if (!includeInactive && !this.#active({ ...meta, purra_state: view.state }))
|
|
451
581
|
return undefined;
|
|
@@ -520,17 +650,15 @@ export class Mem0Memory {
|
|
|
520
650
|
async extract(messages, options) {
|
|
521
651
|
if (!this.#allowInference)
|
|
522
652
|
throw new MemoryError("memory_inference_disabled");
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
throw new TypeError("source messages exceed maxInputChars");
|
|
533
|
-
return this.#write("extract", copied, { ...options, state: "pending" });
|
|
653
|
+
const input = captureInput(messages, options.source, options.metadata, options.expiresAt, this.#maxInput);
|
|
654
|
+
const authorization = options.authorization === undefined ? undefined : authorizationCopy(options.authorization);
|
|
655
|
+
const expectedIntent = "sha256:" + digest(["purra.mem0.capture-intent/v1", input.messages,
|
|
656
|
+
[input.source.id, input.source.revision], input.metadata, input.expiresAt]);
|
|
657
|
+
if (authorization !== undefined && authorization.intentDigest !== expectedIntent) {
|
|
658
|
+
throw new MemoryError("memory_capture_authorization_mismatch");
|
|
659
|
+
}
|
|
660
|
+
return this.#write("extract", input.messages, { ...options, source: input.source, metadata: input.metadata,
|
|
661
|
+
expiresAt: input.expiresAt, ...(authorization === undefined ? {} : { authorization }), state: "pending" });
|
|
534
662
|
}
|
|
535
663
|
async update(id, text, options) {
|
|
536
664
|
return this.#write("update", text, options, id, options.version);
|
|
@@ -587,13 +715,34 @@ export class Mem0Memory {
|
|
|
587
715
|
const state = options.state ?? "active", reason = options.reason == null ? null : requiredText(options.reason, "state reason", 128);
|
|
588
716
|
if (!["active", "pending", "disabled"].includes(state))
|
|
589
717
|
throw new TypeError("invalid memory state");
|
|
590
|
-
const
|
|
591
|
-
|
|
718
|
+
const authorization = options.authorization === undefined ? null : authorizationCopy(options.authorization);
|
|
719
|
+
const authorizationData = authorization === null ? null : {
|
|
720
|
+
intent_digest: authorization.intentDigest, policy_id: authorization.policyId,
|
|
721
|
+
policy_revision: authorization.policyRevision, principal_id: authorization.principalId,
|
|
722
|
+
decision_id: authorization.decisionId,
|
|
723
|
+
};
|
|
724
|
+
const fingerprintInput = [kind, content, source ? [source.id, source.revision] : null, target, version,
|
|
725
|
+
preserveExpiry ? "preserve" : expires, preserveMetadata ? "preserve" : metadata, state, reason];
|
|
726
|
+
if (authorizationData !== null)
|
|
727
|
+
fingerprintInput.push(authorizationData);
|
|
728
|
+
const fingerprint = digest(fingerprintInput);
|
|
592
729
|
const plan = { kind, target, meta: null };
|
|
730
|
+
if (authorizationData !== null)
|
|
731
|
+
plan.capture_authorization = authorizationData;
|
|
593
732
|
if (this.#providers)
|
|
594
733
|
plan.budget = this.#providers.budget.key;
|
|
595
734
|
return this.#call(async () => {
|
|
596
|
-
|
|
735
|
+
let previous = this.#journal.operation(key);
|
|
736
|
+
if (previous) {
|
|
737
|
+
if (previous.fingerprint !== fingerprint)
|
|
738
|
+
throw new MemoryError("memory_idempotency_conflict");
|
|
739
|
+
if (previous.state !== "complete")
|
|
740
|
+
throw new MemoryError("memory_operation_unresolved");
|
|
741
|
+
return this.operation(key);
|
|
742
|
+
}
|
|
743
|
+
if (authorizationData !== null)
|
|
744
|
+
this.#journal.assertCaptureAuthorization(authorizationData);
|
|
745
|
+
previous = this.#journal.begin(key, fingerprint, plan);
|
|
597
746
|
if (previous) {
|
|
598
747
|
if (previous.state !== "complete")
|
|
599
748
|
throw new MemoryError("memory_operation_unresolved");
|
|
@@ -609,13 +758,17 @@ export class Mem0Memory {
|
|
|
609
758
|
if (source)
|
|
610
759
|
this.#journal.assertSource({ purra_source: source.id, purra_revision: source.revision });
|
|
611
760
|
let old;
|
|
761
|
+
let oldMeta;
|
|
612
762
|
if (target !== null) {
|
|
613
763
|
old = await this.#read(target, true, true);
|
|
614
764
|
if (!old)
|
|
615
765
|
throw new MemoryError("memory_not_found");
|
|
616
766
|
if (old.version !== version)
|
|
617
767
|
throw new MemoryError("memory_version_conflict");
|
|
768
|
+
oldMeta = this.#journal.item(target).meta;
|
|
618
769
|
}
|
|
770
|
+
if (authorizationData !== null)
|
|
771
|
+
this.#journal.assertCaptureAuthorization(authorizationData);
|
|
619
772
|
const meta = {
|
|
620
773
|
purra_scope: this.#scope, purra_store: this.#journal.store,
|
|
621
774
|
purra_operation: digest([this.#journal.store, this.#scope, key]),
|
|
@@ -628,10 +781,18 @@ export class Mem0Memory {
|
|
|
628
781
|
purra_reason: old ? old.reason : reason,
|
|
629
782
|
purra_created: old ? old.createdAt : new Date().toISOString(), purra_updated: new Date().toISOString(),
|
|
630
783
|
};
|
|
784
|
+
const capturePolicy = oldMeta?.purra_capture_policy ?? authorizationData?.policy_id;
|
|
785
|
+
const captureDecision = oldMeta?.purra_capture_decision ?? authorizationData?.decision_id;
|
|
786
|
+
if (capturePolicy !== undefined) {
|
|
787
|
+
meta.purra_capture_policy = capturePolicy;
|
|
788
|
+
meta.purra_capture_decision = captureDecision;
|
|
789
|
+
}
|
|
631
790
|
const desiredText = kind === "delete" ? old.text : content;
|
|
632
791
|
plan.meta = meta;
|
|
633
792
|
plan.hash = kind === "extract" ? null : digest(desiredText);
|
|
634
793
|
this.#journal.savePlan(key, plan);
|
|
794
|
+
if (authorizationData !== null)
|
|
795
|
+
this.#journal.assertCaptureAuthorization(authorizationData);
|
|
635
796
|
dispatched = true;
|
|
636
797
|
let ids;
|
|
637
798
|
if (kind === "add" || kind === "extract") {
|
package/dist/workflow.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class MemoryWorkflow {
|
|
|
13
13
|
policy?: MemoryDecisionPolicy;
|
|
14
14
|
policyRevision?: string;
|
|
15
15
|
reviewLimit?: number;
|
|
16
|
+
capturePolicyId?: string;
|
|
17
|
+
capturePolicyRevision?: string;
|
|
16
18
|
});
|
|
17
19
|
capture(messages: Parameters<Mem0Memory["extract"]>[0], options: Parameters<Mem0Memory["extract"]>[1]): Promise<MemoryWorkflowResult>;
|
|
18
20
|
}
|
package/dist/workflow.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { Mem0Memory, requiredText, positiveInteger } from "./memory.js";
|
|
2
|
+
import { Mem0Memory, memoryCaptureIntent, requiredText, positiveInteger } from "./memory.js";
|
|
3
|
+
import { MemoryError } from "./journal.js";
|
|
3
4
|
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
4
5
|
/** Reuse the journal on retry; authorization remains an explicit host policy. */
|
|
5
6
|
export class MemoryWorkflow {
|
|
@@ -7,6 +8,7 @@ export class MemoryWorkflow {
|
|
|
7
8
|
#policy;
|
|
8
9
|
#revision;
|
|
9
10
|
#limit;
|
|
11
|
+
#capturePolicy;
|
|
10
12
|
constructor(memory, options = {}) {
|
|
11
13
|
this.#memory = memory;
|
|
12
14
|
this.#policy = options.policy;
|
|
@@ -14,9 +16,32 @@ export class MemoryWorkflow {
|
|
|
14
16
|
throw new TypeError("policy must be callable");
|
|
15
17
|
this.#revision = requiredText(options.policyRevision ?? "review-only", "policy revision", 512);
|
|
16
18
|
this.#limit = positiveInteger(options.reviewLimit ?? 8, "review limit", 32);
|
|
19
|
+
if ((options.capturePolicyId === undefined) !== (options.capturePolicyRevision === undefined)) {
|
|
20
|
+
throw new TypeError("capture policy id and revision must be configured together");
|
|
21
|
+
}
|
|
22
|
+
this.#capturePolicy = options.capturePolicyId === undefined ? undefined : Object.freeze({
|
|
23
|
+
id: requiredText(options.capturePolicyId, "capture policy id", 512),
|
|
24
|
+
revision: requiredText(options.capturePolicyRevision, "capture policy revision", 512),
|
|
25
|
+
});
|
|
17
26
|
}
|
|
18
27
|
async capture(messages, options) {
|
|
19
28
|
const prefix = "workflow:" + digest(requiredText(options.key, "workflow key", 512));
|
|
29
|
+
if (!this.#capturePolicy) {
|
|
30
|
+
if (options.authorization !== undefined)
|
|
31
|
+
throw new TypeError("capture authorization requires a configured capture policy");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (options.authorization === undefined)
|
|
35
|
+
throw new MemoryError("memory_capture_authorization_required");
|
|
36
|
+
const intentOptions = { source: options.source, maxInputChars: this.#memory.maxInputChars,
|
|
37
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
38
|
+
...(options.expiresAt === undefined ? {} : { expiresAt: options.expiresAt }) };
|
|
39
|
+
const intent = memoryCaptureIntent(messages, intentOptions);
|
|
40
|
+
if (options.authorization.intentDigest !== intent || options.authorization.policyId !== this.#capturePolicy.id
|
|
41
|
+
|| options.authorization.policyRevision !== this.#capturePolicy.revision) {
|
|
42
|
+
throw new MemoryError("memory_capture_authorization_mismatch");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
20
45
|
const extraction = await this.#memory.extract(messages, { ...options, key: prefix + ":extract" });
|
|
21
46
|
const resolutions = [];
|
|
22
47
|
const pendingIds = [];
|
|
@@ -36,7 +61,8 @@ export class MemoryWorkflow {
|
|
|
36
61
|
if (!record || record.state !== "pending")
|
|
37
62
|
continue;
|
|
38
63
|
const reviewed = await this.#memory.review({ id, version: record.version }, {
|
|
39
|
-
key: prefix + ":review:" + suffix, limit: this.#limit,
|
|
64
|
+
key: prefix + ":review:" + suffix, limit: this.#limit,
|
|
65
|
+
...(options.authorization === undefined ? {} : { authorization: options.authorization }), ...signal,
|
|
40
66
|
});
|
|
41
67
|
if (reviewed.state !== "complete" || !reviewed.review || !this.#policy) {
|
|
42
68
|
pendingIds.push(id);
|
|
@@ -50,7 +76,8 @@ export class MemoryWorkflow {
|
|
|
50
76
|
if (decision.reviewKey !== reviewed.review.key || !decision.items.some(ref => ref.id === id && ref.version === record.version)) {
|
|
51
77
|
throw new TypeError("Workflow decision must include the candidate and its review key");
|
|
52
78
|
}
|
|
53
|
-
const resolved = await this.#memory.resolve(decision, { key: resolveKey,
|
|
79
|
+
const resolved = await this.#memory.resolve(decision, { key: resolveKey,
|
|
80
|
+
...(options.authorization === undefined ? {} : { authorization: options.authorization }), ...signal });
|
|
54
81
|
resolutions.push(resolved);
|
|
55
82
|
if (resolved.state !== "complete")
|
|
56
83
|
pendingIds.push(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "purra-mem0",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Scoped Mem0 OSS memory integration for PurrA",
|
|
5
5
|
"license": "MIT AND Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"mysql2": "^3.0.0",
|
|
70
70
|
"@zilliz/milvus2-sdk-node": "^2.4.0 || ^3.0.0",
|
|
71
71
|
"@aws-sdk/client-bedrock-runtime": ">=3.0.0 <3.968.0",
|
|
72
|
-
"purra": "1.0.
|
|
72
|
+
"purra": "1.0.4"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/node": "22.20.1",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"dependencies": {
|
|
185
185
|
"axios": "^1.18.0",
|
|
186
186
|
"openai": "^4.93.0",
|
|
187
|
-
"uuid": "^11.
|
|
187
|
+
"uuid": "^11.0.1",
|
|
188
188
|
"zod": "^3.24.1"
|
|
189
189
|
}
|
|
190
190
|
}
|