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 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: (request: ContextRequest) => string;
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 (typeof this.#query !== "function" || typeof this.#countTokens !== "function")
18
- throw new TypeError("query and countTokens must be host functions");
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] ?? 0;
25
- if (!allowance)
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
- const hits = await this.#memory.retrieve({ query: this.#query(request), limit: this.#limit, scope: {} }, signal);
29
- const result = await assembleMemoryContext(this.#memory, hits.map(hit => hit.id), allowance, { name: this.#name, countTokens: this.#countTokens, expectedEpoch: epoch, ...(signal ? { signal } : {}) });
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
- if (!Number.isSafeInteger(allowance) || allowance < 0 || allowance > 1_000_000)
36
- throw new TypeError("invalid context allowance");
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
- const countTokens = options.countTokens ?? estimateJsonTokens;
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): Promise<MemoryOperation>;
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: SourceOptions): Promise<MemoryOperation>;
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 fingerprint = digest(reviewKey === undefined ? ["resolve", data] : ["resolve", data, reviewKey]);
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 fingerprint = digest(["review", ref, limit, policy]);
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
- const previous = this.#journal.begin(key, fingerprint, plan);
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
- if (!Array.isArray(messages) || messages.length < 1 || messages.length > 100)
524
- throw new TypeError("messages must contain 1 to 100 source messages");
525
- const copied = messages.map(message => {
526
- if (!message || Object.keys(message).sort().join(",") !== "content,role" || !["user", "assistant"].includes(message.role)) {
527
- throw new TypeError("source messages may contain only user/assistant text");
528
- }
529
- return { role: message.role, content: requiredText(message.content, "message", this.#maxInput) };
530
- });
531
- if (copied.reduce((total, m) => total + [...m.content].length, 0) > this.#maxInput)
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 fingerprint = digest([kind, content, source ? [source.id, source.revision] : null, target, version, preserveExpiry ? "preserve" : expires,
591
- preserveMetadata ? "preserve" : metadata, state, reason]);
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
- const previous = this.#journal.begin(key, fingerprint, plan);
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") {
@@ -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, ...signal,
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, ...signal });
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.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.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.1.1",
187
+ "uuid": "^11.0.1",
188
188
  "zod": "^3.24.1"
189
189
  }
190
190
  }