@dsh-enhanced/personal-memory 0.1.7 → 0.1.14

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/lib/store.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { isAbsolute } from 'node:path';
3
+ import { validatePreferenceMemoryPromotionCancellationRequest, withPreferenceMemoryPromotionCancellationReceiptDigest, withPreferenceMemoryPromotionResultDigest, } from '@dsh-enhanced/assistant-growth-contract';
3
4
  import { APPROVAL_DISPLAY_BUDGET } from '@dsh-enhanced/assistant-policy';
4
5
  import { MemoryDatabaseError, openMemoryDatabase } from './sqlite.js';
5
6
  import { tokenizeMemory } from './tokenize.js';
@@ -11,7 +12,146 @@ export class MemoryStoreError extends Error {
11
12
  this.name = 'MemoryStoreError';
12
13
  }
13
14
  }
14
- function normalizeIdentity(identity) {
15
+ const SHA_256 = /^[0-9a-f]{64}$/u;
16
+ function boundedText(value, label, maxBytes = 512) {
17
+ if (typeof value !== 'string' || value.normalize('NFC').trim() !== value || value === ''
18
+ || Buffer.byteLength(value, 'utf8') > maxBytes) {
19
+ throw new MemoryStoreError('invalid-identity', `${label} is invalid`);
20
+ }
21
+ return value;
22
+ }
23
+ export function memoryPrincipalDigest(principal) {
24
+ return createHash('sha256').update(boundedText(principal, 'memory principal', 4_096)).digest('hex');
25
+ }
26
+ export function normalizeMemoryOwnerNamespace(namespace) {
27
+ if (typeof namespace !== 'object' || namespace === null || !SHA_256.test(namespace.principalDigest)) {
28
+ throw new MemoryStoreError('invalid-identity', 'memory owner namespace is invalid');
29
+ }
30
+ if (namespace.mode === 'delivery') {
31
+ if (!Number.isSafeInteger(namespace.principalVersion) || namespace.principalVersion < 1) {
32
+ throw new MemoryStoreError('invalid-identity', 'memory Delivery namespace version is invalid');
33
+ }
34
+ return Object.freeze({
35
+ mode: 'delivery',
36
+ principalDigest: namespace.principalDigest,
37
+ principalRecordId: boundedText(namespace.principalRecordId, 'memory principal record id'),
38
+ principalVersion: namespace.principalVersion,
39
+ });
40
+ }
41
+ if (namespace.mode === 'headless') {
42
+ if (!Number.isSafeInteger(namespace.lineageVersion) || namespace.lineageVersion < 1) {
43
+ throw new MemoryStoreError('invalid-identity', 'memory headless namespace version is invalid');
44
+ }
45
+ return Object.freeze({
46
+ mode: 'headless',
47
+ principalDigest: namespace.principalDigest,
48
+ lineageId: boundedText(namespace.lineageId, 'memory headless lineage id'),
49
+ lineageVersion: namespace.lineageVersion,
50
+ });
51
+ }
52
+ throw new MemoryStoreError('invalid-identity', 'memory owner namespace mode is invalid');
53
+ }
54
+ export function memoryOwnerNamespaceKey(namespace) {
55
+ const normalized = normalizeMemoryOwnerNamespace(namespace);
56
+ const durableIdentity = normalized.mode === 'delivery'
57
+ ? {
58
+ mode: normalized.mode,
59
+ principalRecordId: normalized.principalRecordId,
60
+ principalVersion: normalized.principalVersion,
61
+ }
62
+ : {
63
+ mode: normalized.mode,
64
+ principalDigest: normalized.principalDigest,
65
+ lineageId: normalized.lineageId,
66
+ lineageVersion: normalized.lineageVersion,
67
+ };
68
+ return createHash('sha256').update(stableJson(durableIdentity)).digest('hex');
69
+ }
70
+ function namespaceColumns(namespace) {
71
+ const normalized = normalizeMemoryOwnerNamespace(namespace);
72
+ return {
73
+ namespaceMode: normalized.mode,
74
+ namespaceKey: memoryOwnerNamespaceKey(normalized),
75
+ principalDigest: normalized.principalDigest,
76
+ principalRecordId: normalized.mode === 'delivery' ? normalized.principalRecordId : '',
77
+ principalVersion: normalized.mode === 'delivery' ? normalized.principalVersion : 0,
78
+ headlessLineageId: normalized.mode === 'headless' ? normalized.lineageId : '',
79
+ headlessLineageVersion: normalized.mode === 'headless' ? normalized.lineageVersion : 0,
80
+ };
81
+ }
82
+ function namespaceSqlValues(namespace) {
83
+ return [
84
+ namespace.namespaceMode,
85
+ namespace.namespaceKey,
86
+ namespace.principalDigest,
87
+ namespace.namespaceMode === 'delivery' ? namespace.principalRecordId : null,
88
+ namespace.namespaceMode === 'delivery' ? namespace.principalVersion : null,
89
+ namespace.namespaceMode === 'headless' ? namespace.headlessLineageId : null,
90
+ namespace.namespaceMode === 'headless' ? namespace.headlessLineageVersion : null,
91
+ ];
92
+ }
93
+ function namespaceFromRow(row) {
94
+ if (row.namespace_mode === 'legacy-quarantine' || row.principal_digest === null) {
95
+ throw new MemoryStoreError('invalid-identity', 'legacy memory owner namespace is quarantined');
96
+ }
97
+ const namespace = row.namespace_mode === 'delivery'
98
+ ? {
99
+ mode: 'delivery',
100
+ principalDigest: row.principal_digest,
101
+ principalRecordId: row.principal_record_id,
102
+ principalVersion: row.principal_version,
103
+ }
104
+ : {
105
+ mode: 'headless',
106
+ principalDigest: row.principal_digest,
107
+ lineageId: row.headless_lineage_id,
108
+ lineageVersion: row.headless_lineage_version,
109
+ };
110
+ const normalized = normalizeMemoryOwnerNamespace(namespace);
111
+ if (memoryOwnerNamespaceKey(normalized) !== row.namespace_key) {
112
+ throw new MemoryStoreError('invalid-identity', 'stored memory owner namespace digest is invalid');
113
+ }
114
+ return normalized;
115
+ }
116
+ function namespaceMatchesRow(row, namespace) {
117
+ return row.namespace_key === memoryOwnerNamespaceKey(namespace);
118
+ }
119
+ function stableValue(value) {
120
+ if (Array.isArray(value))
121
+ return value.map(stableValue);
122
+ if (typeof value !== 'object' || value === null)
123
+ return value;
124
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right, 'en'))
125
+ .map(([key, nested]) => [key, stableValue(nested)]));
126
+ }
127
+ function stableJson(value) {
128
+ return JSON.stringify(stableValue(value));
129
+ }
130
+ function validatePromotionReference(value) {
131
+ if (value === undefined)
132
+ return undefined;
133
+ if (!Number.isSafeInteger(value.promotionGeneration) || value.promotionGeneration < 1
134
+ || !Number.isSafeInteger(value.ownerGeneration) || value.ownerGeneration < 1
135
+ || !SHA_256.test(value.requestDigest)
136
+ || typeof value.scope !== 'object' || value.scope === null
137
+ || !isAbsolute(value.scope.workspace)) {
138
+ throw new MemoryStoreError('invalid-entry', 'memory promotion reference is invalid');
139
+ }
140
+ return Object.freeze({
141
+ promotionId: boundedText(value.promotionId, 'memory promotion id'),
142
+ promotionGeneration: value.promotionGeneration,
143
+ requestDigest: value.requestDigest,
144
+ ownerGeneration: value.ownerGeneration,
145
+ scope: Object.freeze({
146
+ workspace: value.scope.workspace,
147
+ preset: boundedText(value.scope.preset, 'memory promotion preset'),
148
+ }),
149
+ ...(value.prePolicyStatus === undefined ? {} : {
150
+ prePolicyStatus: value.prePolicyStatus,
151
+ }),
152
+ });
153
+ }
154
+ function normalizeIdentity(identity, namespace) {
15
155
  if (identity.owner !== 'user' && identity.owner !== 'agent') {
16
156
  throw new MemoryStoreError('invalid-identity', 'memory owner must be user or agent');
17
157
  }
@@ -35,6 +175,7 @@ function normalizeIdentity(identity) {
35
175
  throw new MemoryStoreError('invalid-identity', 'user-owned memory must not carry an agent preset');
36
176
  }
37
177
  return {
178
+ ...namespaceColumns(namespace),
38
179
  owner: identity.owner,
39
180
  scope: identity.scope,
40
181
  workspace: identity.workspace ?? '',
@@ -51,7 +192,11 @@ function normalizeAgentContext(context) {
51
192
  if (context.agentPreset.trim() === '') {
52
193
  throw new MemoryStoreError('invalid-identity', 'memory search requires a non-empty agent preset');
53
194
  }
54
- return { workspace: context.workspace, agentPreset: context.agentPreset };
195
+ return {
196
+ workspace: context.workspace,
197
+ agentPreset: context.agentPreset,
198
+ namespace: normalizeMemoryOwnerNamespace(context.namespace),
199
+ };
55
200
  }
56
201
  function publicIdentity(identity) {
57
202
  return {
@@ -81,16 +226,19 @@ function contentHash(content) {
81
226
  export function hashMemoryMutation(mutation) {
82
227
  return createHash('sha256').update(JSON.stringify(mutation)).digest('hex');
83
228
  }
229
+ export function hashMemoryProposalIntent(input) {
230
+ return createHash('sha256').update(stableJson({
231
+ mutation: input.mutation,
232
+ namespace: normalizeMemoryOwnerNamespace(input.namespace),
233
+ promotion: validatePromotionReference(input.promotion) ?? null,
234
+ ttlMs: input.ttlMs,
235
+ notAfter: input.notAfter,
236
+ dispatch: input.dispatch ?? null,
237
+ })).digest('hex');
238
+ }
84
239
  export function missingPolicyProposalId(proposalId, ttlMs, dispatch) {
85
- const canonicalDispatch = dispatch === undefined
86
- ? null
87
- : {
88
- sourceId: dispatch.sourceId,
89
- bindingId: dispatch.bindingId,
90
- workspace: dispatch.workspace,
91
- principal: dispatch.principal,
92
- };
93
- const fingerprint = createHash('sha256').update(JSON.stringify({
240
+ const canonicalDispatch = dispatch ?? null;
241
+ const fingerprint = createHash('sha256').update(stableJson({
94
242
  proposalId,
95
243
  ttlMs,
96
244
  dispatch: canonicalDispatch,
@@ -131,6 +279,20 @@ export class MemoryStore {
131
279
  this.#closed = true;
132
280
  this.#database.close();
133
281
  }
282
+ resolveProposalNotAfter(namespace, idempotencyKey, ttlMs, requested) {
283
+ const key = memoryOwnerNamespaceKey(namespace);
284
+ const existing = this.#proposalByIdempotencyKey(key, idempotencyKey)
285
+ ?? this.#proposalIntentByIdempotencyKey(key, idempotencyKey);
286
+ if (existing !== undefined)
287
+ return existing.not_after;
288
+ const now = this.#now();
289
+ const notAfter = requested ?? now + ttlMs;
290
+ if (!Number.isSafeInteger(now) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0
291
+ || !Number.isSafeInteger(notAfter) || notAfter < 0 || notAfter > now + ttlMs) {
292
+ throw new MemoryStoreError('invalid-entry', 'proposal deadline is invalid');
293
+ }
294
+ return notAfter;
295
+ }
134
296
  #transaction(operation) {
135
297
  this.#database.exec('BEGIN IMMEDIATE');
136
298
  try {
@@ -143,20 +305,20 @@ export class MemoryStore {
143
305
  throw error;
144
306
  }
145
307
  }
146
- get(identity, id) {
147
- const columns = normalizeIdentity(identity);
308
+ get(namespace, identity, id) {
309
+ const columns = normalizeIdentity(identity, namespace);
148
310
  const row = this.#selectRecord(columns, id);
149
311
  if (row === undefined || row.status !== 'active' || this.#expired(row))
150
312
  return undefined;
151
313
  return this.#toRecord(row);
152
314
  }
153
- list(identity, options = {}) {
154
- const columns = normalizeIdentity(identity);
315
+ list(namespace, identity, options = {}) {
316
+ const columns = normalizeIdentity(identity, namespace);
155
317
  const rows = this.#database.prepare(`
156
318
  SELECT * FROM memory_records
157
- WHERE owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
319
+ WHERE namespace_key = ? AND owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
158
320
  ORDER BY created_at ASC, id ASC
159
- `).all(columns.owner, columns.scope, columns.workspace, columns.agentPreset);
321
+ `).all(columns.namespaceKey, columns.owner, columns.scope, columns.workspace, columns.agentPreset);
160
322
  return rows
161
323
  .filter(row => row.status === 'removed' ? options.includeRemoved === true : !this.#expired(row))
162
324
  .map(row => this.#toRecord(row));
@@ -183,11 +345,14 @@ export class MemoryStore {
183
345
  SUM(CASE WHEN status = 'removed' THEN 1 ELSE 0 END) AS removed_records,
184
346
  SUM(CASE WHEN status = 'active' AND expires_at IS NOT NULL AND expires_at <= ? THEN 1 ELSE 0 END) AS expired_records
185
347
  FROM memory_records
348
+ WHERE namespace_mode <> 'legacy-quarantine'
186
349
  `).get(now, now);
187
350
  const proposals = this.#database.prepare(`
188
351
  SELECT
189
- (SELECT COUNT(*) FROM memory_proposals WHERE status = 'pending')
190
- + (SELECT COUNT(*) FROM memory_proposal_intents) AS count
352
+ (SELECT COUNT(*) FROM memory_proposals
353
+ WHERE namespace_mode <> 'legacy-quarantine' AND status = 'pending')
354
+ + (SELECT COUNT(*) FROM memory_proposal_intents
355
+ WHERE namespace_mode <> 'legacy-quarantine') AS count
191
356
  `).get();
192
357
  return {
193
358
  activeRecords: row.active_records ?? 0,
@@ -316,9 +481,9 @@ export class MemoryStore {
316
481
  records: Object.freeze(records),
317
482
  });
318
483
  }
319
- normalizeMutation(mutation, options = {}) {
484
+ normalizeMutation(mutation, options) {
320
485
  const preflight = options.preflight !== false;
321
- const columns = normalizeIdentity(mutation.identity);
486
+ const columns = normalizeIdentity(mutation.identity, options.namespace);
322
487
  const identity = publicIdentity(columns);
323
488
  if (mutation.op === 'add') {
324
489
  const entry = this.#publicEntry(this.#validateEntry(mutation.entry));
@@ -360,10 +525,14 @@ export class MemoryStore {
360
525
  }
361
526
  prepareProposalIntent(input) {
362
527
  const mutationJson = JSON.stringify(input.mutation);
528
+ const namespace = namespaceColumns(input.namespace);
529
+ const promotion = validatePromotionReference(input.promotion);
530
+ const promotionJson = promotion === undefined ? null : JSON.stringify(promotion);
531
+ const dispatchJson = input.dispatch === undefined ? null : stableJson(input.dispatch);
363
532
  const diff = JSON.stringify(input.mutation, null, 2);
364
533
  if (input.proposalId.trim() === '' || input.idempotencyKey.trim() === ''
365
534
  || input.requester.trim() === '' || input.principal.trim() === ''
366
- || !/^[0-9a-f]{64}$/u.test(input.mutationHash)
535
+ || input.mutationHash !== hashMemoryProposalIntent(input)
367
536
  || !Number.isSafeInteger(input.ttlMs) || input.ttlMs <= 0
368
537
  || Buffer.byteLength(`personal-memory:${input.idempotencyKey}`, 'utf8') > 512
369
538
  || Buffer.byteLength(input.requester, 'utf8') > 512
@@ -371,6 +540,9 @@ export class MemoryStore {
371
540
  || Buffer.byteLength(diff, 'utf8') > APPROVAL_DISPLAY_BUDGET.maxDiffBytes) {
372
541
  throw new MemoryStoreError('invalid-entry', 'proposal intent fields are invalid');
373
542
  }
543
+ if (input.namespace.principalDigest !== memoryPrincipalDigest(input.principal)) {
544
+ throw new MemoryStoreError('invalid-identity', 'proposal principal does not match its owner namespace');
545
+ }
374
546
  if (input.dispatch !== undefined
375
547
  && (input.dispatch.sourceId.trim() === '' || input.dispatch.bindingId.trim() === ''
376
548
  || !isAbsolute(input.dispatch.workspace) || input.dispatch.principal !== input.principal
@@ -381,10 +553,20 @@ export class MemoryStore {
381
553
  throw new MemoryStoreError('invalid-entry', 'proposal intent dispatch route is invalid');
382
554
  }
383
555
  return this.#transaction(() => {
556
+ if (promotion !== undefined) {
557
+ const cancellation = this.#promotionCancellation(promotion.promotionId, promotion.promotionGeneration);
558
+ if (cancellation !== undefined) {
559
+ this.#assertCancellationMatchesPromotion(cancellation, promotion, input.namespace);
560
+ return Object.freeze({
561
+ kind: 'cancelled',
562
+ receipt: this.#cancellationReceiptFromRow(cancellation, 'replayed'),
563
+ });
564
+ }
565
+ }
384
566
  // The proposal and its creation intent live in separate tables. Resolve
385
567
  // both under the same write lock so another process cannot attach a local
386
568
  // proposal between an absence check and this intent insert.
387
- const proposalByKey = this.#proposalByIdempotencyKey(input.idempotencyKey);
569
+ const proposalByKey = this.#proposalByIdempotencyKey(namespace.namespaceKey, input.idempotencyKey);
388
570
  const proposalById = this.#proposal(input.proposalId);
389
571
  if (proposalByKey !== undefined || proposalById !== undefined) {
390
572
  const existing = proposalByKey ?? proposalById;
@@ -392,8 +574,9 @@ export class MemoryStore {
392
574
  // possible aliases before returning/raising outside the transaction so
393
575
  // poison work cannot permanently occupy the reconcile lane.
394
576
  this.#database.prepare(`
395
- DELETE FROM memory_proposal_intents WHERE id = ? OR idempotency_key = ?
396
- `).run(input.proposalId, input.idempotencyKey);
577
+ DELETE FROM memory_proposal_intents
578
+ WHERE id = ? OR (namespace_key = ? AND idempotency_key = ?)
579
+ `).run(input.proposalId, namespace.namespaceKey, input.idempotencyKey);
397
580
  const sameRow = proposalByKey === undefined || proposalById === undefined
398
581
  || proposalByKey.id === proposalById.id;
399
582
  if (!sameRow || !this.#proposalMatchesPrepareInput(existing, input, mutationJson)) {
@@ -401,7 +584,7 @@ export class MemoryStore {
401
584
  }
402
585
  return Object.freeze({ kind: 'proposal', proposal: this.#toProposal(existing) });
403
586
  }
404
- const intentByKey = this.#proposalIntentByIdempotencyKey(input.idempotencyKey);
587
+ const intentByKey = this.#proposalIntentByIdempotencyKey(namespace.namespaceKey, input.idempotencyKey);
405
588
  const intentById = this.#proposalIntent(input.proposalId);
406
589
  if (intentByKey !== undefined || intentById !== undefined) {
407
590
  const existing = intentByKey ?? intentById;
@@ -418,21 +601,25 @@ export class MemoryStore {
418
601
  }
419
602
  // Keep the mutation preflight in the same transaction as the absence
420
603
  // checks. Final application still repeats every CAS/duplicate check.
421
- const preflight = this.normalizeMutation(input.mutation);
604
+ const preflight = this.normalizeMutation(input.mutation, { namespace: input.namespace });
422
605
  if (JSON.stringify(preflight) !== mutationJson) {
423
606
  throw new MemoryStoreError('invalid-entry', 'proposal mutation is not canonical');
424
607
  }
425
608
  const now = this.#now();
426
- if (!Number.isSafeInteger(now) || !Number.isSafeInteger(now + input.ttlMs)) {
609
+ const notAfter = input.notAfter;
610
+ if (!Number.isSafeInteger(now) || !Number.isSafeInteger(now + input.ttlMs)
611
+ || !Number.isSafeInteger(notAfter) || notAfter < 0 || notAfter > now + input.ttlMs
612
+ || (notAfter <= now && promotion?.prePolicyStatus === undefined)) {
427
613
  throw new MemoryStoreError('invalid-entry', 'proposal intent deadline exceeds the safe timestamp range');
428
614
  }
429
615
  this.#database.prepare(`
430
616
  INSERT INTO memory_proposal_intents(
431
- id, idempotency_key, requester, principal, mutation_hash, mutation_json, ttl_ms,
432
- dispatch_source_id, dispatch_binding_id, dispatch_workspace, dispatch_principal,
617
+ id, namespace_mode, namespace_key, principal_digest, principal_record_id, principal_version,
618
+ headless_lineage_id, headless_lineage_version, idempotency_key, requester,
619
+ principal, mutation_hash, mutation_json, promotion_json, ttl_ms, not_after, dispatch_json,
433
620
  created_at, updated_at
434
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
435
- `).run(input.proposalId, input.idempotencyKey, input.requester, input.principal, input.mutationHash, mutationJson, input.ttlMs, input.dispatch?.sourceId ?? null, input.dispatch?.bindingId ?? null, input.dispatch?.workspace ?? null, input.dispatch?.principal ?? null, now, now);
621
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
622
+ `).run(input.proposalId, ...namespaceSqlValues(namespace), input.idempotencyKey, input.requester, input.principal, input.mutationHash, mutationJson, promotionJson, input.ttlMs, notAfter, dispatchJson, now, now);
436
623
  return Object.freeze({
437
624
  kind: 'intent',
438
625
  intent: this.#toProposalIntent(this.#requiredProposalIntent(input.proposalId)),
@@ -447,7 +634,9 @@ export class MemoryStore {
447
634
  listProposalIntents(limit) {
448
635
  this.#validateProposalListLimit(limit);
449
636
  const rows = this.#database.prepare(`
450
- SELECT * FROM memory_proposal_intents ORDER BY updated_at, id LIMIT ?
637
+ SELECT * FROM memory_proposal_intents
638
+ WHERE namespace_mode <> 'legacy-quarantine'
639
+ ORDER BY updated_at, id LIMIT ?
451
640
  `).all(limit);
452
641
  return rows.map(row => this.#toProposalIntent(row));
453
642
  }
@@ -460,7 +649,7 @@ export class MemoryStore {
460
649
  `).run(now, now, proposalId);
461
650
  }
462
651
  /** Fail closed after Policy atomically abandons or rejects exact recovery. */
463
- conflictProposalIntent(proposalId) {
652
+ conflictProposalIntent(proposalId, promotion) {
464
653
  return this.#transaction(() => {
465
654
  const existing = this.#proposal(proposalId);
466
655
  if (existing !== undefined) {
@@ -470,29 +659,44 @@ export class MemoryStore {
470
659
  this.#database.prepare(`
471
660
  DELETE FROM memory_proposal_intents WHERE id = ? AND idempotency_key = ?
472
661
  `).run(existing.id, existing.idempotency_key);
473
- return { proposal: this.#toProposal(existing), replayed: true };
662
+ const proposal = this.#toProposal(existing);
663
+ if (promotion !== undefined)
664
+ this.#enqueuePromotionResult(proposal, undefined, promotion);
665
+ return { proposal, replayed: true };
474
666
  }
475
667
  const intent = this.#requiredProposalIntent(proposalId);
476
- const expiresAt = intent.created_at + intent.ttl_ms;
668
+ const expiresAt = intent.not_after;
477
669
  if (!Number.isSafeInteger(expiresAt)) {
478
670
  throw new MemoryStoreError('invalid-entry', 'proposal intent expiry exceeds the safe timestamp range');
479
671
  }
480
- return this.#materializeProposalIntentConflict(intent, expiresAt, this.#now());
672
+ const settled = this.#materializeProposalIntentConflict(intent, expiresAt, this.#now());
673
+ const terminal = promotion ?? (settled.proposal.promotion === undefined
674
+ ? undefined
675
+ : settled.proposal.promotion);
676
+ if (terminal !== undefined)
677
+ this.#enqueuePromotionResult(settled.proposal, undefined, terminal);
678
+ return settled;
481
679
  });
482
680
  }
483
681
  saveProposal(input) {
484
- if (!Number.isSafeInteger(input.expiresAt) || !Number.isSafeInteger(input.version) || input.version <= 0) {
682
+ const namespace = namespaceColumns(input.namespace);
683
+ const promotion = validatePromotionReference(input.promotion);
684
+ if (!Number.isSafeInteger(input.expiresAt) || !Number.isSafeInteger(input.notAfter)
685
+ || input.expiresAt > input.notAfter || !Number.isSafeInteger(input.version) || input.version <= 0) {
485
686
  throw new MemoryStoreError('invalid-entry', 'proposal expiry and version must be safe integers');
486
687
  }
487
688
  return this.#transaction(() => {
488
- const existing = this.#proposalByIdempotencyKey(input.idempotencyKey);
689
+ const existing = this.#proposalByIdempotencyKey(namespace.namespaceKey, input.idempotencyKey);
489
690
  if (existing !== undefined) {
490
691
  const same = existing.id === input.proposalId
491
692
  && existing.policy_proposal_id === input.policyProposalId
492
693
  && existing.requester === input.requester
493
694
  && existing.principal === input.principal
695
+ && existing.namespace_key === namespace.namespaceKey
494
696
  && existing.mutation_hash === input.mutationHash
495
- && existing.expires_at === input.expiresAt;
697
+ && existing.not_after === input.notAfter
698
+ && existing.expires_at === input.expiresAt
699
+ && existing.promotion_json === (promotion === undefined ? null : JSON.stringify(promotion));
496
700
  if (!same)
497
701
  throw new MemoryStoreError('idempotency-conflict', 'proposal key was used for another mutation');
498
702
  this.#database.prepare('DELETE FROM memory_proposal_intents WHERE id = ?').run(input.proposalId);
@@ -501,10 +705,12 @@ export class MemoryStore {
501
705
  const now = this.#now();
502
706
  this.#database.prepare(`
503
707
  INSERT INTO memory_proposals(
504
- id, policy_proposal_id, idempotency_key, requester, principal,
505
- mutation_hash, mutation_json, status, expires_at, created_at, updated_at, version
506
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)
507
- `).run(input.proposalId, input.policyProposalId, input.idempotencyKey, input.requester, input.principal, input.mutationHash, JSON.stringify(input.mutation), input.expiresAt, now, now, input.version);
708
+ id, namespace_mode, namespace_key, principal_digest, principal_record_id, principal_version,
709
+ headless_lineage_id, headless_lineage_version, policy_proposal_id, idempotency_key,
710
+ requester, principal, mutation_hash, mutation_json, promotion_json, status, not_after, expires_at,
711
+ created_at, updated_at, version
712
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
713
+ `).run(input.proposalId, ...namespaceSqlValues(namespace), input.policyProposalId, input.idempotencyKey, input.requester, input.principal, input.mutationHash, JSON.stringify(input.mutation), promotion === undefined ? null : JSON.stringify(promotion), input.notAfter, input.expiresAt, now, now, input.version);
508
714
  this.#database.prepare('DELETE FROM memory_proposal_intents WHERE id = ?').run(input.proposalId);
509
715
  return { proposal: this.#toProposal(this.#requiredProposal(input.proposalId)), replayed: false };
510
716
  });
@@ -520,7 +726,8 @@ export class MemoryStore {
520
726
  listPendingProposals(limit) {
521
727
  this.#validateProposalListLimit(limit);
522
728
  const rows = this.#database.prepare(`
523
- SELECT * FROM memory_proposals WHERE status = 'pending'
729
+ SELECT * FROM memory_proposals
730
+ WHERE namespace_mode <> 'legacy-quarantine' AND status = 'pending'
524
731
  ORDER BY updated_at, id LIMIT ?
525
732
  `).all(limit);
526
733
  return rows.map(row => this.#toProposal(row));
@@ -533,14 +740,211 @@ export class MemoryStore {
533
740
  WHERE id = ? AND status = 'pending'
534
741
  `).run(now, now, proposalId);
535
742
  }
743
+ listPendingPromotionResults(limit) {
744
+ this.#validateProposalListLimit(limit);
745
+ const rows = this.#database.prepare(`
746
+ SELECT * FROM memory_promotion_results
747
+ WHERE state = 'pending' ORDER BY updated_at, promotion_id, promotion_generation LIMIT ?
748
+ `).all(limit);
749
+ return rows.map(row => this.#toPromotionResult(row));
750
+ }
751
+ getPromotionResult(promotionId, promotionGeneration, resultVersion) {
752
+ if (!Number.isSafeInteger(promotionGeneration) || promotionGeneration < 1
753
+ || !Number.isSafeInteger(resultVersion) || resultVersion < 1) {
754
+ throw new MemoryStoreError('invalid-entry', 'memory promotion result identity is invalid');
755
+ }
756
+ const row = this.#database.prepare(`
757
+ SELECT * FROM memory_promotion_results
758
+ WHERE promotion_id = ? AND promotion_generation = ? AND result_version = ?
759
+ `).get(boundedText(promotionId, 'memory promotion id'), promotionGeneration, resultVersion);
760
+ return row === undefined ? undefined : this.#toPromotionResult(row);
761
+ }
762
+ getProposalByPromotion(promotionId, promotionGeneration) {
763
+ if (!Number.isSafeInteger(promotionGeneration) || promotionGeneration < 1) {
764
+ throw new MemoryStoreError('invalid-entry', 'memory promotion identity is invalid');
765
+ }
766
+ const id = boundedText(promotionId, 'memory promotion id');
767
+ const match = (json) => {
768
+ if (json === null)
769
+ return undefined;
770
+ const reference = validatePromotionReference(JSON.parse(json));
771
+ return reference?.promotionId === id && reference.promotionGeneration === promotionGeneration
772
+ ? reference
773
+ : undefined;
774
+ };
775
+ for (const row of this.#database.prepare(`
776
+ SELECT * FROM memory_proposals WHERE promotion_json IS NOT NULL ORDER BY updated_at, id
777
+ `).all()) {
778
+ const reference = match(row.promotion_json);
779
+ if (reference !== undefined)
780
+ return Object.freeze({
781
+ kind: 'proposal',
782
+ proposal: this.#toProposal(row),
783
+ requestDigest: reference.requestDigest,
784
+ namespace: namespaceFromRow(row),
785
+ });
786
+ }
787
+ for (const row of this.#database.prepare(`
788
+ SELECT * FROM memory_proposal_intents WHERE promotion_json IS NOT NULL ORDER BY updated_at, id
789
+ `).all()) {
790
+ const reference = match(row.promotion_json);
791
+ if (reference !== undefined)
792
+ return Object.freeze({
793
+ kind: 'intent',
794
+ proposal: this.#toProposalIntent(row),
795
+ requestDigest: reference.requestDigest,
796
+ namespace: namespaceFromRow(row),
797
+ });
798
+ }
799
+ return undefined;
800
+ }
801
+ cancelPromotionBeforeOrAfterSubmit(input) {
802
+ const request = validatePreferenceMemoryPromotionCancellationRequest(input);
803
+ return this.#transaction(() => {
804
+ const existing = this.#promotionCancellation(request.promotionId, request.promotionGeneration);
805
+ if (existing !== undefined) {
806
+ const privacyEscalation = existing.reason === 'superseded' && request.reason !== 'superseded';
807
+ if (privacyEscalation) {
808
+ this.#assertCancellationTargetMatchesRequest(existing, request);
809
+ const result = this.#database.prepare(`
810
+ SELECT * FROM memory_promotion_results
811
+ WHERE promotion_id = ? AND promotion_generation = ?
812
+ `).get(request.promotionId, request.promotionGeneration);
813
+ if (result?.status === 'confirmed') {
814
+ this.#assertCancellationMatchesResult(request, result);
815
+ this.#compensateConfirmedPromotion(request, result);
816
+ }
817
+ const receipt = this.#cancellationReceipt(request, 'cancelled');
818
+ const now = this.#now();
819
+ this.#database.prepare(`
820
+ UPDATE memory_promotion_cancellations SET
821
+ cancellation_digest = ?, reason = ?, occurred_at = ?, receipt_digest = ?, updated_at = ?
822
+ WHERE promotion_id = ? AND promotion_generation = ? AND reason = 'superseded'
823
+ `).run(request.cancellationDigest, request.reason, request.occurredAt, receipt.receiptDigest, now, request.promotionId, request.promotionGeneration);
824
+ return Object.freeze({ outcome: 'cancelled', receipt });
825
+ }
826
+ this.#assertCancellationIdentityMatchesRequest(existing, request);
827
+ const validStoredReceipt = this.#validStoredCancellationReceipt(existing);
828
+ if (!validStoredReceipt) {
829
+ throw new MemoryStoreError('idempotency-conflict', 'promotion cancellation receipt changed');
830
+ }
831
+ const result = this.#database.prepare(`
832
+ SELECT * FROM memory_promotion_results
833
+ WHERE promotion_id = ? AND promotion_generation = ?
834
+ `).get(request.promotionId, request.promotionGeneration);
835
+ if (result?.status === 'confirmed' && request.reason !== 'superseded') {
836
+ this.#assertCancellationMatchesResult(request, result);
837
+ this.#compensateConfirmedPromotion(request, result);
838
+ }
839
+ const legacyPrivacyAlreadyConfirmed = request.reason !== 'superseded'
840
+ && existing.receipt_digest === this.#cancellationReceiptFromRow(existing, 'already-confirmed').receiptDigest;
841
+ if (legacyPrivacyAlreadyConfirmed) {
842
+ if (result?.status !== 'confirmed') {
843
+ throw new MemoryStoreError('idempotency-conflict', 'legacy privacy cancellation lost its confirmed result');
844
+ }
845
+ const receipt = this.#cancellationReceipt(request, 'cancelled');
846
+ this.#database.prepare(`
847
+ UPDATE memory_promotion_cancellations SET receipt_digest = ?, updated_at = ?
848
+ WHERE promotion_id = ? AND promotion_generation = ? AND receipt_digest = ?
849
+ `).run(receipt.receiptDigest, this.#now(), request.promotionId, request.promotionGeneration, existing.receipt_digest);
850
+ return Object.freeze({ outcome: 'cancelled', receipt });
851
+ }
852
+ const receipt = this.#cancellationReceiptFromRow(existing, 'replayed');
853
+ return Object.freeze({ outcome: 'replayed', receipt });
854
+ }
855
+ const result = this.#database.prepare(`
856
+ SELECT * FROM memory_promotion_results
857
+ WHERE promotion_id = ? AND promotion_generation = ?
858
+ `).get(request.promotionId, request.promotionGeneration);
859
+ let outcome = 'cancelled';
860
+ if (result !== undefined) {
861
+ this.#assertCancellationMatchesResult(request, result);
862
+ if (result.status === 'confirmed') {
863
+ if (request.reason === 'superseded')
864
+ outcome = 'already-confirmed';
865
+ else
866
+ this.#compensateConfirmedPromotion(request, result);
867
+ }
868
+ }
869
+ const located = result?.status === 'confirmed'
870
+ ? undefined
871
+ : this.getProposalByPromotion(request.promotionId, request.promotionGeneration);
872
+ if (located !== undefined) {
873
+ this.#assertCancellationMatchesProposal(request, located.proposal);
874
+ if (located.kind === 'proposal' && located.proposal.status === 'approved') {
875
+ throw new MemoryStoreError('idempotency-conflict', 'confirmed promotion lost its durable terminal result');
876
+ }
877
+ else {
878
+ const terminal = located.kind === 'intent'
879
+ ? this.#materializeProposalIntentConflict(this.#requiredProposalIntent(located.proposal.proposalId), located.proposal.notAfter, this.#now()).proposal
880
+ : this.#conflictPendingProposalInCurrentTransaction(located.proposal.proposalId);
881
+ if (terminal.promotion !== undefined) {
882
+ this.#enqueuePromotionResult(terminal, undefined, terminal.promotion);
883
+ }
884
+ }
885
+ }
886
+ const receipt = this.#cancellationReceipt(request, outcome);
887
+ const now = this.#now();
888
+ this.#database.prepare(`
889
+ INSERT INTO memory_promotion_cancellations(
890
+ promotion_id, promotion_generation, request_digest, principal_record_id, principal_version,
891
+ owner_generation, cancellation_digest, reason, occurred_at, receipt_digest, created_at, updated_at
892
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
893
+ `).run(request.promotionId, request.promotionGeneration, request.requestDigest, request.principalLineage.principalRecordId, request.principalLineage.principalVersion, request.ownerGeneration, request.cancellationDigest, request.reason, request.occurredAt, receipt.receiptDigest, now, now);
894
+ return Object.freeze({ outcome, receipt });
895
+ });
896
+ }
897
+ completePromotionResult(result) {
898
+ return this.#transaction(() => {
899
+ const current = this.#requiredExactPromotionResult(result);
900
+ if (current.state === 'completed')
901
+ return false;
902
+ const changed = this.#database.prepare(`
903
+ UPDATE memory_promotion_results SET state = 'completed', updated_at = ?
904
+ WHERE promotion_id = ? AND promotion_generation = ? AND result_version = ?
905
+ AND receipt_digest = ? AND state = 'pending' AND attempt_count = ?
906
+ `).run(this.#now(), result.promotionId, result.promotionGeneration, result.resultVersion, result.receiptDigest, result.attemptCount);
907
+ if (changed.changes !== 1)
908
+ throw new MemoryStoreError('version-conflict', 'promotion result changed');
909
+ return true;
910
+ });
911
+ }
912
+ deferPromotionResult(result, error) {
913
+ if (error !== undefined && Buffer.byteLength(error, 'utf8') > 2_048) {
914
+ throw new MemoryStoreError('invalid-entry', 'promotion delivery error is too large');
915
+ }
916
+ return this.#transaction(() => {
917
+ const current = this.#requiredExactPromotionResult(result);
918
+ if (current.state === 'completed')
919
+ return false;
920
+ const changed = this.#database.prepare(`
921
+ UPDATE memory_promotion_results SET attempt_count = attempt_count + 1, last_error = ?, updated_at = ?
922
+ WHERE promotion_id = ? AND promotion_generation = ? AND result_version = ?
923
+ AND receipt_digest = ? AND state = 'pending' AND attempt_count = ?
924
+ `).run(error ?? null, this.#now(), result.promotionId, result.promotionGeneration, result.resultVersion, result.receiptDigest, result.attemptCount);
925
+ if (changed.changes !== 1)
926
+ throw new MemoryStoreError('version-conflict', 'promotion result changed');
927
+ return true;
928
+ });
929
+ }
536
930
  settleProposal(input) {
537
931
  const existing = this.#requiredProposal(input.proposalId);
932
+ if (existing.namespace_mode === 'legacy-quarantine') {
933
+ throw new MemoryStoreError('invalid-identity', 'legacy memory proposal is quarantined');
934
+ }
935
+ const promotion = input.promotion
936
+ ?? (existing.promotion_json === null ? undefined : {
937
+ ...JSON.parse(existing.promotion_json),
938
+ });
538
939
  if (existing.status !== 'pending') {
539
- return {
940
+ const settled = {
540
941
  proposal: this.#toProposal(existing),
541
942
  ...this.#proposalRecord(existing),
542
943
  replayed: true,
543
944
  };
945
+ if (promotion !== undefined)
946
+ this.#transaction(() => this.#enqueuePromotionResult(settled.proposal, settled.record, promotion));
947
+ return settled;
544
948
  }
545
949
  if (input.policyStatus !== 'approved') {
546
950
  return this.#transaction(() => {
@@ -558,8 +962,11 @@ export class MemoryStore {
558
962
  UPDATE memory_proposals SET status = ?, updated_at = ?, version = ?
559
963
  WHERE id = ? AND status = 'pending'
560
964
  `).run(input.policyStatus, this.#now(), version, input.proposalId);
965
+ const proposal = this.#toProposal(this.#requiredProposal(input.proposalId));
966
+ if (promotion !== undefined)
967
+ this.#enqueuePromotionResult(proposal, undefined, promotion);
561
968
  return {
562
- proposal: this.#toProposal(this.#requiredProposal(input.proposalId)),
969
+ proposal,
563
970
  replayed: false,
564
971
  };
565
972
  });
@@ -577,14 +984,18 @@ export class MemoryStore {
577
984
  const record = this.#applyMutationInCurrentTransaction({
578
985
  ...mutation,
579
986
  idempotencyKey: `memory-proposal:${current.id}`,
987
+ namespace: namespaceFromRow(current),
580
988
  });
581
989
  this.#database.prepare(`
582
990
  UPDATE memory_proposals
583
991
  SET status = 'approved', result_memory_id = ?, updated_at = ?, version = ?
584
992
  WHERE id = ? AND status = 'pending'
585
993
  `).run(record.id, this.#now(), input.policyVersion, input.proposalId);
994
+ const proposal = this.#toProposal(this.#requiredProposal(input.proposalId));
995
+ if (promotion !== undefined)
996
+ this.#enqueuePromotionResult(proposal, record, promotion);
586
997
  return {
587
- proposal: this.#toProposal(this.#requiredProposal(input.proposalId)),
998
+ proposal,
588
999
  record,
589
1000
  replayed: false,
590
1001
  };
@@ -604,6 +1015,8 @@ export class MemoryStore {
604
1015
  `).run(this.#now(), input.policyVersion, input.proposalId);
605
1016
  }
606
1017
  const conflicted = this.#requiredProposal(input.proposalId);
1018
+ if (promotion !== undefined)
1019
+ this.#enqueuePromotionResult(this.#toProposal(conflicted), undefined, promotion);
607
1020
  return { proposal: this.#toProposal(conflicted), replayed: false };
608
1021
  });
609
1022
  }
@@ -612,11 +1025,12 @@ export class MemoryStore {
612
1025
  return this.#transaction(() => this.#applyMutationInCurrentTransaction(mutation));
613
1026
  }
614
1027
  #applyMutationInCurrentTransaction(mutation) {
615
- const identity = normalizeIdentity(mutation.identity);
1028
+ const identity = normalizeIdentity(mutation.identity, mutation.namespace);
616
1029
  const hash = hashMemoryMutation(this.#withoutIdempotencyKey(mutation));
617
1030
  const prior = this.#database.prepare(`
618
- SELECT mutation_hash, memory_id FROM memory_audit WHERE idempotency_key = ?
619
- `).get(mutation.idempotencyKey);
1031
+ SELECT mutation_hash, memory_id FROM memory_audit
1032
+ WHERE namespace_key = ? AND idempotency_key = ?
1033
+ `).get(identity.namespaceKey, mutation.idempotencyKey);
620
1034
  if (prior !== undefined) {
621
1035
  if (prior.mutation_hash !== hash) {
622
1036
  throw new MemoryStoreError('idempotency-conflict', 'idempotency key was used for another mutation');
@@ -633,9 +1047,11 @@ export class MemoryStore {
633
1047
  : this.#remove(identity, mutation.id, mutation.expectedVersion);
634
1048
  this.#database.prepare(`
635
1049
  INSERT INTO memory_audit(
636
- idempotency_key, mutation_hash, operation, memory_id, result_version, occurred_at
637
- ) VALUES (?, ?, ?, ?, ?, ?)
638
- `).run(mutation.idempotencyKey, hash, mutation.op, result.id, result.version, this.#now());
1050
+ namespace_mode, namespace_key, principal_digest, principal_record_id, principal_version,
1051
+ headless_lineage_id, headless_lineage_version, idempotency_key, mutation_hash,
1052
+ operation, memory_id, result_version, occurred_at
1053
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1054
+ `).run(...namespaceSqlValues(identity), mutation.idempotencyKey, hash, mutation.op, result.id, result.version, this.#now());
639
1055
  return result;
640
1056
  }
641
1057
  #add(identity, input) {
@@ -646,20 +1062,22 @@ export class MemoryStore {
646
1062
  const now = this.#now();
647
1063
  this.#database.prepare(`
648
1064
  INSERT INTO memory_records(
649
- id, owner, scope, workspace, agent_preset, kind, content, content_hash,
1065
+ id, namespace_mode, namespace_key, principal_digest, principal_record_id, principal_version,
1066
+ headless_lineage_id, headless_lineage_version,
1067
+ owner, scope, workspace, agent_preset, kind, content, content_hash,
650
1068
  sensitivity, trust, confidence, provenance_json, supersedes, expires_at,
651
1069
  status, created_at, updated_at, version
652
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, 1)
653
- `).run(id, identity.owner, identity.scope, identity.workspace, identity.agentPreset, entry.kind, entry.content, entry.contentHash, entry.sensitivity, entry.trust, entry.confidence, JSON.stringify(entry.provenance), entry.supersedes ?? null, entry.expiresAt ?? null, now, now);
1070
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, 1)
1071
+ `).run(id, ...namespaceSqlValues(identity), identity.owner, identity.scope, identity.workspace, identity.agentPreset, entry.kind, entry.content, entry.contentHash, entry.sensitivity, entry.trust, entry.confidence, JSON.stringify(entry.provenance), entry.supersedes ?? null, entry.expiresAt ?? null, now, now);
654
1072
  this.#replaceTokens(id, entry.content);
655
1073
  return this.#toRecord(this.#selectRecord(identity, id));
656
1074
  }
657
1075
  #assertRecordCapacity(identity) {
658
1076
  const count = this.#database.prepare(`
659
1077
  SELECT COUNT(*) AS count FROM memory_records
660
- WHERE owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
1078
+ WHERE namespace_key = ? AND owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
661
1079
  AND status = 'active' AND (expires_at IS NULL OR expires_at > ?)
662
- `).get(identity.owner, identity.scope, identity.workspace, identity.agentPreset, this.#now());
1080
+ `).get(identity.namespaceKey, identity.owner, identity.scope, identity.workspace, identity.agentPreset, this.#now());
663
1081
  if (count.count >= this.#maxRecordsPerIdentity) {
664
1082
  throw new MemoryStoreError('record-limit', 'memory identity record limit is reached');
665
1083
  }
@@ -733,12 +1151,12 @@ export class MemoryStore {
733
1151
  #assertNoDuplicate(identity, hash, excludedId) {
734
1152
  const duplicate = this.#database.prepare(`
735
1153
  SELECT id FROM memory_records
736
- WHERE owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
1154
+ WHERE namespace_key = ? AND owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
737
1155
  AND content_hash = ? AND status = 'active'
738
1156
  AND (expires_at IS NULL OR expires_at > ?)
739
1157
  AND (? IS NULL OR id <> ?)
740
1158
  LIMIT 1
741
- `).get(identity.owner, identity.scope, identity.workspace, identity.agentPreset, hash, this.#now(), excludedId ?? null, excludedId ?? null);
1159
+ `).get(identity.namespaceKey, identity.owner, identity.scope, identity.workspace, identity.agentPreset, hash, this.#now(), excludedId ?? null, excludedId ?? null);
742
1160
  if (duplicate !== undefined)
743
1161
  throw new MemoryStoreError('duplicate-content', 'active memory content already exists');
744
1162
  }
@@ -780,8 +1198,8 @@ export class MemoryStore {
780
1198
  #selectRecord(identity, id) {
781
1199
  return this.#database.prepare(`
782
1200
  SELECT * FROM memory_records
783
- WHERE id = ? AND owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
784
- `).get(id, identity.owner, identity.scope, identity.workspace, identity.agentPreset);
1201
+ WHERE id = ? AND namespace_key = ? AND owner = ? AND scope = ? AND workspace = ? AND agent_preset = ?
1202
+ `).get(id, identity.namespaceKey, identity.owner, identity.scope, identity.workspace, identity.agentPreset);
785
1203
  }
786
1204
  #validateProposalListLimit(limit) {
787
1205
  if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1_000) {
@@ -792,9 +1210,10 @@ export class MemoryStore {
792
1210
  return this.#database.prepare('SELECT * FROM memory_proposal_intents WHERE id = ?')
793
1211
  .get(id);
794
1212
  }
795
- #proposalIntentByIdempotencyKey(key) {
796
- return this.#database.prepare('SELECT * FROM memory_proposal_intents WHERE idempotency_key = ?')
797
- .get(key);
1213
+ #proposalIntentByIdempotencyKey(namespaceKey, key) {
1214
+ return this.#database.prepare(`
1215
+ SELECT * FROM memory_proposal_intents WHERE namespace_key = ? AND idempotency_key = ?
1216
+ `).get(namespaceKey, key);
798
1217
  }
799
1218
  #requiredProposalIntent(id) {
800
1219
  const intent = this.#proposalIntent(id);
@@ -803,72 +1222,70 @@ export class MemoryStore {
803
1222
  return intent;
804
1223
  }
805
1224
  #materializeProposalIntentConflict(intent, expiresAt, now) {
806
- const dispatch = intent.dispatch_source_id === null
1225
+ const dispatch = intent.dispatch_json === null
807
1226
  ? undefined
808
- : {
809
- sourceId: intent.dispatch_source_id,
810
- bindingId: intent.dispatch_binding_id,
811
- workspace: intent.dispatch_workspace,
812
- principal: intent.dispatch_principal,
813
- };
1227
+ : JSON.parse(intent.dispatch_json);
814
1228
  this.#database.prepare(`
815
1229
  INSERT INTO memory_proposals(
816
- id, policy_proposal_id, idempotency_key, requester, principal,
817
- mutation_hash, mutation_json, status, expires_at, created_at, updated_at, version
818
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'conflicted', ?, ?, ?, 2)
819
- `).run(intent.id, missingPolicyProposalId(intent.id, intent.ttl_ms, dispatch), intent.idempotency_key, intent.requester, intent.principal, intent.mutation_hash, intent.mutation_json, expiresAt, intent.created_at, now);
1230
+ id, namespace_mode, namespace_key, principal_digest, principal_record_id, principal_version,
1231
+ headless_lineage_id, headless_lineage_version, policy_proposal_id, idempotency_key,
1232
+ requester, principal, mutation_hash, mutation_json, promotion_json, status, not_after, expires_at,
1233
+ created_at, updated_at, version
1234
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'conflicted', ?, ?, ?, ?, 2)
1235
+ `).run(intent.id, intent.namespace_mode, intent.namespace_key, intent.principal_digest, intent.principal_record_id, intent.principal_version, intent.headless_lineage_id, intent.headless_lineage_version, missingPolicyProposalId(intent.id, intent.ttl_ms, dispatch), intent.idempotency_key, intent.requester, intent.principal, intent.mutation_hash, intent.mutation_json, intent.promotion_json, intent.not_after, expiresAt, intent.created_at, now);
820
1236
  this.#database.prepare('DELETE FROM memory_proposal_intents WHERE id = ?').run(intent.id);
821
1237
  return { proposal: this.#toProposal(this.#requiredProposal(intent.id)), replayed: false };
822
1238
  }
823
1239
  #intentDispatchMatches(row, dispatch) {
824
- if (dispatch === undefined)
825
- return row.dispatch_source_id === null
826
- && row.dispatch_binding_id === null
827
- && row.dispatch_workspace === null
828
- && row.dispatch_principal === null;
829
- return row.dispatch_source_id === dispatch.sourceId
830
- && row.dispatch_binding_id === dispatch.bindingId
831
- && row.dispatch_workspace === dispatch.workspace
832
- && row.dispatch_principal === dispatch.principal;
1240
+ return row.dispatch_json === (dispatch === undefined ? null : stableJson(dispatch));
833
1241
  }
834
1242
  #proposalMatchesPrepareInput(row, input, mutationJson) {
835
1243
  return row.id === input.proposalId
836
1244
  && row.idempotency_key === input.idempotencyKey
837
1245
  && row.requester === input.requester
838
1246
  && row.principal === input.principal
1247
+ && namespaceMatchesRow(row, input.namespace)
839
1248
  && row.mutation_json === mutationJson
840
- && (row.mutation_hash === input.mutationHash
841
- || row.mutation_hash === hashMemoryMutation(input.mutation));
1249
+ && row.mutation_hash === input.mutationHash
1250
+ && row.not_after === input.notAfter
1251
+ && row.promotion_json === (input.promotion === undefined
1252
+ ? null
1253
+ : JSON.stringify(validatePromotionReference(input.promotion)));
842
1254
  }
843
1255
  #intentMatchesPrepareInput(row, input, mutationJson) {
844
1256
  return row.id === input.proposalId
845
1257
  && row.idempotency_key === input.idempotencyKey
846
1258
  && row.requester === input.requester
847
1259
  && row.principal === input.principal
1260
+ && namespaceMatchesRow(row, input.namespace)
848
1261
  && row.mutation_json === mutationJson
849
- && (row.mutation_hash === input.mutationHash
850
- || row.mutation_hash === hashMemoryMutation(input.mutation))
1262
+ && row.mutation_hash === input.mutationHash
851
1263
  && row.ttl_ms === input.ttlMs
1264
+ && row.not_after === input.notAfter
1265
+ && row.promotion_json === (input.promotion === undefined
1266
+ ? null
1267
+ : JSON.stringify(validatePromotionReference(input.promotion)))
852
1268
  && this.#intentDispatchMatches(row, input.dispatch);
853
1269
  }
854
1270
  #toProposalIntent(row) {
855
- const dispatch = row.dispatch_source_id === null
1271
+ const dispatch = row.dispatch_json === null
856
1272
  ? undefined
857
- : Object.freeze({
858
- sourceId: row.dispatch_source_id,
859
- bindingId: row.dispatch_binding_id,
860
- workspace: row.dispatch_workspace,
861
- principal: row.dispatch_principal,
862
- });
1273
+ : Object.freeze(JSON.parse(row.dispatch_json));
1274
+ const promotion = row.promotion_json === null
1275
+ ? undefined
1276
+ : Object.freeze(JSON.parse(row.promotion_json));
863
1277
  return Object.freeze({
864
1278
  proposalId: row.id,
865
1279
  idempotencyKey: row.idempotency_key,
866
1280
  requester: row.requester,
867
1281
  principal: row.principal,
1282
+ namespace: namespaceFromRow(row),
868
1283
  mutationHash: row.mutation_hash,
869
1284
  mutation: Object.freeze(JSON.parse(row.mutation_json)),
870
1285
  ttlMs: row.ttl_ms,
1286
+ notAfter: row.not_after,
871
1287
  ...(dispatch === undefined ? {} : { dispatch }),
1288
+ ...(promotion === undefined ? {} : { promotion }),
872
1289
  createdAt: row.created_at,
873
1290
  updatedAt: row.updated_at,
874
1291
  });
@@ -877,9 +1294,10 @@ export class MemoryStore {
877
1294
  return this.#database.prepare('SELECT * FROM memory_proposals WHERE id = ?')
878
1295
  .get(id);
879
1296
  }
880
- #proposalByIdempotencyKey(key) {
881
- return this.#database.prepare('SELECT * FROM memory_proposals WHERE idempotency_key = ?')
882
- .get(key);
1297
+ #proposalByIdempotencyKey(namespaceKey, key) {
1298
+ return this.#database.prepare(`
1299
+ SELECT * FROM memory_proposals WHERE namespace_key = ? AND idempotency_key = ?
1300
+ `).get(namespaceKey, key);
883
1301
  }
884
1302
  #requiredProposal(id) {
885
1303
  const proposal = this.#proposal(id);
@@ -887,13 +1305,273 @@ export class MemoryStore {
887
1305
  throw new MemoryStoreError('not-found', 'memory proposal was not found');
888
1306
  return proposal;
889
1307
  }
1308
+ #promotionCancellation(id, generation) {
1309
+ return this.#database.prepare(`
1310
+ SELECT * FROM memory_promotion_cancellations
1311
+ WHERE promotion_id = ? AND promotion_generation = ?
1312
+ `).get(id, generation);
1313
+ }
1314
+ #assertCancellationMatchesPromotion(cancellation, promotion, namespace) {
1315
+ if (namespace.mode !== 'delivery'
1316
+ || cancellation.request_digest !== promotion.requestDigest
1317
+ || cancellation.principal_record_id !== namespace.principalRecordId
1318
+ || cancellation.principal_version !== namespace.principalVersion
1319
+ || cancellation.owner_generation !== promotion.ownerGeneration) {
1320
+ throw new MemoryStoreError('idempotency-conflict', 'cancelled promotion identity changed');
1321
+ }
1322
+ }
1323
+ #assertCancellationIdentityMatchesRequest(cancellation, request) {
1324
+ if (cancellation.request_digest !== request.requestDigest
1325
+ || cancellation.principal_record_id !== request.principalLineage.principalRecordId
1326
+ || cancellation.principal_version !== request.principalLineage.principalVersion
1327
+ || cancellation.owner_generation !== request.ownerGeneration
1328
+ || cancellation.cancellation_digest !== request.cancellationDigest
1329
+ || cancellation.reason !== request.reason
1330
+ || cancellation.occurred_at !== request.occurredAt) {
1331
+ throw new MemoryStoreError('idempotency-conflict', 'promotion cancellation identity changed');
1332
+ }
1333
+ }
1334
+ #assertCancellationTargetMatchesRequest(cancellation, request) {
1335
+ if (cancellation.request_digest !== request.requestDigest
1336
+ || cancellation.principal_record_id !== request.principalLineage.principalRecordId
1337
+ || cancellation.principal_version !== request.principalLineage.principalVersion
1338
+ || cancellation.owner_generation !== request.ownerGeneration) {
1339
+ throw new MemoryStoreError('idempotency-conflict', 'promotion cancellation target changed');
1340
+ }
1341
+ }
1342
+ #assertCancellationMatchesProposal(request, proposal) {
1343
+ const promotion = proposal.promotion;
1344
+ if (promotion === undefined || proposal.namespace.mode !== 'delivery'
1345
+ || promotion.requestDigest !== request.requestDigest
1346
+ || promotion.ownerGeneration !== request.ownerGeneration
1347
+ || proposal.namespace.principalRecordId !== request.principalLineage.principalRecordId
1348
+ || proposal.namespace.principalVersion !== request.principalLineage.principalVersion) {
1349
+ throw new MemoryStoreError('idempotency-conflict', 'promotion cancellation does not match its proposal');
1350
+ }
1351
+ }
1352
+ #assertCancellationMatchesResult(request, result) {
1353
+ if (result.request_digest !== request.requestDigest
1354
+ || result.principal_record_id !== request.principalLineage.principalRecordId
1355
+ || result.principal_version !== request.principalLineage.principalVersion
1356
+ || result.owner_generation !== request.ownerGeneration) {
1357
+ throw new MemoryStoreError('idempotency-conflict', 'promotion cancellation does not match its result');
1358
+ }
1359
+ }
1360
+ #compensateConfirmedPromotion(request, result) {
1361
+ if (result.status !== 'confirmed' || result.memory_record_id === null
1362
+ || result.memory_record_version === null || result.memory_record_digest === null) {
1363
+ throw new MemoryStoreError('idempotency-conflict', 'confirmed promotion result is incomplete');
1364
+ }
1365
+ const prior = this.#database.prepare(`
1366
+ SELECT * FROM memory_promotion_compensations
1367
+ WHERE promotion_id = ? AND promotion_generation = ?
1368
+ `).get(request.promotionId, request.promotionGeneration);
1369
+ if (prior !== undefined) {
1370
+ if (prior.request_digest !== request.requestDigest
1371
+ || prior.cancellation_digest !== request.cancellationDigest
1372
+ || prior.memory_proposal_id !== result.memory_proposal_id
1373
+ || prior.memory_proposal_version !== result.memory_proposal_version
1374
+ || prior.memory_record_id !== result.memory_record_id
1375
+ || prior.memory_record_version !== result.memory_record_version
1376
+ || prior.memory_record_digest !== result.memory_record_digest) {
1377
+ throw new MemoryStoreError('idempotency-conflict', 'promotion compensation identity changed');
1378
+ }
1379
+ return;
1380
+ }
1381
+ const record = this.#database.prepare(`
1382
+ SELECT * FROM memory_records WHERE id = ? AND namespace_key = ?
1383
+ `).get(result.memory_record_id, result.namespace_key);
1384
+ if (record === undefined || record.version !== result.memory_record_version
1385
+ || record.content_hash !== result.memory_record_digest || record.status !== 'active') {
1386
+ throw new MemoryStoreError('version-conflict', 'promotion-created Memory record changed before compensation');
1387
+ }
1388
+ const now = this.#now();
1389
+ const removed = this.#database.prepare(`
1390
+ UPDATE memory_records SET status = 'removed', updated_at = ?, version = version + 1
1391
+ WHERE id = ? AND namespace_key = ? AND version = ? AND content_hash = ? AND status = 'active'
1392
+ `).run(now, result.memory_record_id, result.namespace_key, result.memory_record_version, result.memory_record_digest);
1393
+ if (removed.changes !== 1) {
1394
+ throw new MemoryStoreError('version-conflict', 'promotion compensation lost its record CAS');
1395
+ }
1396
+ this.#database.prepare('DELETE FROM memory_tokens WHERE memory_id = ?').run(result.memory_record_id);
1397
+ this.#database.prepare(`
1398
+ INSERT INTO memory_promotion_compensations(
1399
+ promotion_id, promotion_generation, request_digest, cancellation_digest,
1400
+ memory_proposal_id, memory_proposal_version, memory_record_id,
1401
+ memory_record_version, memory_record_digest, removed_record_version, compensated_at
1402
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1403
+ `).run(request.promotionId, request.promotionGeneration, request.requestDigest, request.cancellationDigest, result.memory_proposal_id, result.memory_proposal_version, result.memory_record_id, result.memory_record_version, result.memory_record_digest, result.memory_record_version + 1, now);
1404
+ }
1405
+ #cancellationReceipt(input, outcome) {
1406
+ return withPreferenceMemoryPromotionCancellationReceiptDigest({
1407
+ contractVersion: 1,
1408
+ promotionId: input.promotionId,
1409
+ promotionGeneration: input.promotionGeneration,
1410
+ requestDigest: input.requestDigest,
1411
+ cancellationDigest: input.cancellationDigest,
1412
+ outcome,
1413
+ });
1414
+ }
1415
+ #cancellationReceiptFromRow(row, outcome) {
1416
+ return this.#cancellationReceipt({
1417
+ promotionId: row.promotion_id,
1418
+ promotionGeneration: row.promotion_generation,
1419
+ requestDigest: row.request_digest,
1420
+ cancellationDigest: row.cancellation_digest,
1421
+ }, outcome);
1422
+ }
1423
+ #validStoredCancellationReceipt(row) {
1424
+ return row.receipt_digest === this.#cancellationReceiptFromRow(row, 'cancelled').receiptDigest
1425
+ || row.receipt_digest === this.#cancellationReceiptFromRow(row, 'already-confirmed').receiptDigest;
1426
+ }
1427
+ #conflictPendingProposalInCurrentTransaction(proposalId) {
1428
+ const current = this.#requiredProposal(proposalId);
1429
+ if (current.status === 'pending') {
1430
+ this.#database.prepare(`
1431
+ UPDATE memory_proposals SET status = 'conflicted', updated_at = ?, version = version + 1
1432
+ WHERE id = ? AND status = 'pending'
1433
+ `).run(this.#now(), proposalId);
1434
+ }
1435
+ return this.#toProposal(this.#requiredProposal(proposalId));
1436
+ }
890
1437
  #proposalRecord(row) {
891
1438
  if (row.result_memory_id === null)
892
1439
  return {};
893
1440
  const proposal = this.#toProposal(row);
894
- const record = this.#selectRecord(normalizeIdentity(proposal.mutation.identity), row.result_memory_id);
1441
+ const record = this.#selectRecord(normalizeIdentity(proposal.mutation.identity, proposal.namespace), row.result_memory_id);
895
1442
  return record === undefined ? {} : { record: this.#toRecord(record) };
896
1443
  }
1444
+ #enqueuePromotionResult(proposal, record, input) {
1445
+ const reference = validatePromotionReference(input);
1446
+ if (proposal.promotion !== undefined
1447
+ && JSON.stringify(proposal.promotion) !== JSON.stringify(reference)) {
1448
+ throw new MemoryStoreError('idempotency-conflict', 'promotion settlement does not match its proposal');
1449
+ }
1450
+ if (proposal.status === 'pending') {
1451
+ throw new MemoryStoreError('invalid-entry', 'promotion result requires a terminal proposal');
1452
+ }
1453
+ const status = input.statusOverride
1454
+ ?? (proposal.status === 'approved' ? 'confirmed' : proposal.status);
1455
+ if ((status === 'confirmed') !== (record !== undefined)) {
1456
+ throw new MemoryStoreError('invalid-entry', 'promotion terminal result is inconsistent');
1457
+ }
1458
+ const resultVersion = 1;
1459
+ const namespace = namespaceColumns(proposal.namespace);
1460
+ const existing = this.#database.prepare(`
1461
+ SELECT * FROM memory_promotion_results
1462
+ WHERE promotion_id = ? AND promotion_generation = ?
1463
+ `).get(reference.promotionId, reference.promotionGeneration);
1464
+ if (existing !== undefined) {
1465
+ if (existing.request_digest !== reference.requestDigest
1466
+ || existing.memory_proposal_id !== proposal.proposalId
1467
+ || existing.status !== status
1468
+ || existing.memory_proposal_version !== proposal.version
1469
+ || existing.namespace_key !== namespace.namespaceKey
1470
+ || existing.memory_record_id !== (record?.id ?? null)
1471
+ || existing.memory_record_version !== (record?.version ?? null)
1472
+ || existing.memory_record_digest !== (record?.contentHash ?? null)) {
1473
+ throw new MemoryStoreError('idempotency-conflict', 'promotion result identity was reused');
1474
+ }
1475
+ return this.#toPromotionResult(existing);
1476
+ }
1477
+ const occurredAt = this.#now();
1478
+ const common = {
1479
+ contractVersion: 1,
1480
+ promotionId: reference.promotionId,
1481
+ promotionGeneration: reference.promotionGeneration,
1482
+ requestDigest: reference.requestDigest,
1483
+ resultVersion,
1484
+ occurredAt,
1485
+ };
1486
+ const wire = status === 'stale-owner'
1487
+ ? { ...common, status }
1488
+ : status === 'rejected'
1489
+ ? {
1490
+ ...common, status, rejectionKind: 'owner-explicit',
1491
+ memoryProposalId: proposal.proposalId, memoryProposalVersion: proposal.version,
1492
+ }
1493
+ : status === 'confirmed'
1494
+ ? {
1495
+ ...common, status, memoryProposalId: proposal.proposalId,
1496
+ memoryProposalVersion: proposal.version,
1497
+ memoryRecordId: record.id,
1498
+ memoryRecordVersion: record.version,
1499
+ memoryRecordDigest: record.contentHash,
1500
+ }
1501
+ : {
1502
+ ...common, status, memoryProposalId: proposal.proposalId,
1503
+ memoryProposalVersion: proposal.version,
1504
+ };
1505
+ const receipt = withPreferenceMemoryPromotionResultDigest(wire);
1506
+ const internal = {
1507
+ ...receipt,
1508
+ namespace: proposal.namespace,
1509
+ memoryProposalId: proposal.proposalId,
1510
+ memoryProposalVersion: proposal.version,
1511
+ ...(record === undefined ? {} : {
1512
+ memoryRecordId: record.id,
1513
+ memoryRecordVersion: record.version,
1514
+ memoryRecordDigest: record.contentHash,
1515
+ }),
1516
+ };
1517
+ this.#database.prepare(`
1518
+ INSERT INTO memory_promotion_results(
1519
+ promotion_id, promotion_generation, request_digest, namespace_mode, namespace_key, principal_digest,
1520
+ principal_record_id, principal_version, headless_lineage_id, headless_lineage_version, owner_generation,
1521
+ contract_version, result_version, status, memory_proposal_id, memory_proposal_version, occurred_at,
1522
+ receipt_digest, memory_record_id, memory_record_version, memory_record_digest, state, attempt_count,
1523
+ last_error, created_at, updated_at
1524
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, NULL, ?, ?)
1525
+ `).run(reference.promotionId, reference.promotionGeneration, reference.requestDigest, ...namespaceSqlValues(namespace), reference.ownerGeneration, resultVersion, status, proposal.proposalId, proposal.version, occurredAt, internal.receiptDigest, record?.id ?? null, record?.version ?? null, record?.contentHash ?? null, occurredAt, occurredAt);
1526
+ return this.#toPromotionResult(this.#database.prepare(`
1527
+ SELECT * FROM memory_promotion_results WHERE promotion_id = ? AND promotion_generation = ?
1528
+ `).get(reference.promotionId, reference.promotionGeneration));
1529
+ }
1530
+ #requiredExactPromotionResult(result) {
1531
+ const row = this.#database.prepare(`
1532
+ SELECT * FROM memory_promotion_results WHERE promotion_id = ? AND promotion_generation = ?
1533
+ `).get(result.promotionId, result.promotionGeneration);
1534
+ if (row === undefined)
1535
+ throw new MemoryStoreError('not-found', 'promotion result was not found');
1536
+ const current = this.#toPromotionResult(row);
1537
+ if (current.resultVersion !== result.resultVersion || current.receiptDigest !== result.receiptDigest
1538
+ || current.attemptCount !== result.attemptCount) {
1539
+ throw new MemoryStoreError('version-conflict', 'promotion result changed');
1540
+ }
1541
+ return row;
1542
+ }
1543
+ #toPromotionResult(row) {
1544
+ const namespace = namespaceFromRow(row);
1545
+ return Object.freeze({
1546
+ contractVersion: 1,
1547
+ promotionId: row.promotion_id,
1548
+ promotionGeneration: row.promotion_generation,
1549
+ requestDigest: row.request_digest,
1550
+ scope: this.#promotionScope(row.memory_proposal_id),
1551
+ ownerGeneration: row.owner_generation,
1552
+ namespace,
1553
+ resultVersion: row.result_version,
1554
+ status: row.status,
1555
+ memoryProposalId: row.memory_proposal_id,
1556
+ memoryProposalVersion: row.memory_proposal_version,
1557
+ occurredAt: row.occurred_at,
1558
+ receiptDigest: row.receipt_digest,
1559
+ ...(row.memory_record_id === null ? {} : { memoryRecordId: row.memory_record_id }),
1560
+ ...(row.memory_record_version === null ? {} : { memoryRecordVersion: row.memory_record_version }),
1561
+ ...(row.memory_record_digest === null ? {} : { memoryRecordDigest: row.memory_record_digest }),
1562
+ state: row.state,
1563
+ attemptCount: row.attempt_count,
1564
+ updatedAt: row.updated_at,
1565
+ });
1566
+ }
1567
+ #promotionScope(proposalId) {
1568
+ const row = this.#requiredProposal(proposalId);
1569
+ if (row.promotion_json === null) {
1570
+ throw new MemoryStoreError('invalid-entry', 'promotion result lost its durable proposal reference');
1571
+ }
1572
+ const reference = validatePromotionReference(JSON.parse(row.promotion_json));
1573
+ return reference.scope;
1574
+ }
897
1575
  #toProposal(row) {
898
1576
  return Object.freeze({
899
1577
  proposalId: row.id,
@@ -901,20 +1579,25 @@ export class MemoryStore {
901
1579
  idempotencyKey: row.idempotency_key,
902
1580
  requester: row.requester,
903
1581
  principal: row.principal,
1582
+ namespace: namespaceFromRow(row),
904
1583
  mutationHash: row.mutation_hash,
905
1584
  mutation: Object.freeze(JSON.parse(row.mutation_json)),
906
1585
  status: row.status,
1586
+ notAfter: row.not_after,
907
1587
  expiresAt: row.expires_at,
908
1588
  version: row.version,
909
1589
  ...(row.result_memory_id === null ? {} : { resultMemoryId: row.result_memory_id }),
1590
+ ...(row.promotion_json === null
1591
+ ? {}
1592
+ : { promotion: Object.freeze(JSON.parse(row.promotion_json)) }),
910
1593
  });
911
1594
  }
912
1595
  #visibleRecords(context) {
913
1596
  return [
914
- ...this.list({ owner: 'user', scope: 'user-global' }),
915
- ...this.list({ owner: 'user', scope: 'workspace', workspace: context.workspace }),
916
- ...this.list({ owner: 'agent', scope: 'user-global', agentPreset: context.agentPreset }),
917
- ...this.list({
1597
+ ...this.list(context.namespace, { owner: 'user', scope: 'user-global' }),
1598
+ ...this.list(context.namespace, { owner: 'user', scope: 'workspace', workspace: context.workspace }),
1599
+ ...this.list(context.namespace, { owner: 'agent', scope: 'user-global', agentPreset: context.agentPreset }),
1600
+ ...this.list(context.namespace, {
918
1601
  owner: 'agent',
919
1602
  scope: 'workspace',
920
1603
  workspace: context.workspace,
@@ -942,6 +1625,7 @@ export class MemoryStore {
942
1625
  #toRecord(row) {
943
1626
  return Object.freeze({
944
1627
  id: row.id,
1628
+ namespace: namespaceFromRow(row),
945
1629
  owner: row.owner,
946
1630
  scope: row.scope,
947
1631
  ...(row.workspace === '' ? {} : { workspace: row.workspace }),