@yielded/agent-storage-memory 0.1.0-beta.168 → 0.1.0-beta.170

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.
@@ -50,7 +50,7 @@ interface MemoryDeliveryTransfer {
50
50
  readonly after: string;
51
51
  }, ThreadStoreError>;
52
52
  readonly startImport: (threadId: ThreadId) => Effect.Effect<MemoryDeliveryImport, ThreadImportRejected | ThreadStoreError>;
53
- readonly pendingPeerCount: (threadId: ThreadId, limit: number) => number;
53
+ readonly peerCount: (threadId: ThreadId, nowMillis: number, limit: number) => number;
54
54
  }
55
55
  /** Native control metadata only; callers already hold the journal's shared gate. */
56
56
  interface MemoryWorkOwner {
@@ -82,4 +82,4 @@ declare const MemoryThreadStoreKernel_base: Context.ServiceClass<MemoryThreadSto
82
82
  declare class MemoryThreadStoreKernel extends MemoryThreadStoreKernel_base {}
83
83
  //#endregion
84
84
  export { MemoryThreadStoreKernel as t };
85
- //# sourceMappingURL=MemoryThreadStoreKernel-DYNDjlhO.d.mts.map
85
+ //# sourceMappingURL=MemoryThreadStoreKernel-h_k2t46J.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WorkSelection-CmqTuws3.mjs","names":[],"sources":["../src/internal/MemoryThreadStoreKernel.ts","../src/internal/WorkSelection.ts"],"sourcesContent":["import type { RunId, SubmissionId, ThreadId, ToolCallId } from \"@yielded/agent/identifiers\";\nimport type { IdempotencyKey } from \"@yielded/agent/receipt\";\nimport type { RecordEnvelope, RecordId } from \"@yielded/agent/records\";\nimport type { StorageOwner } from \"@yielded/agent/run-storage\";\nimport type { SubmissionSnapshot, WorkerLedgerState } from \"@yielded/agent/submission-ledger\";\nimport type {\n PreparedImportPage,\n ThreadImportReader,\n ThreadDeliveryImportReader,\n ThreadImportRejected,\n} from \"@yielded/agent/thread-import\";\nimport type {\n AppendResult,\n AppendRequest,\n ThreadStoreFailure,\n ThreadTail,\n ThreadAdmission,\n ThreadStoreError,\n ThreadExportSnapshot,\n} from \"@yielded/agent/thread-store\";\nimport type { TransferFacts, TransferSection } from \"@yielded/agent/thread-transfer\";\nimport type {\n ThreadWorkEntry,\n WorkThreadsRequest,\n WorkThreadsPage,\n} from \"@yielded/agent/thread-work\";\nimport { Context, type Effect, type Stream } from \"effect\";\n\nexport interface PreparedMemoryAppend {\n readonly request: AppendRequest;\n readonly batchJson: string;\n}\n\n/** Private storage, not a whole-Thread preparation value. Publication callbacks never yield. */\nexport interface MemoryLedgerImport {\n readonly reader: Pick<ThreadImportReader[\"Service\"], \"admission\" | \"runOwner\" | \"commands\">;\n readonly stage: (\n page: PreparedImportPage,\n ) => Effect.Effect<void, ThreadImportRejected | ThreadStoreError>;\n readonly prepareCommit: (\n workerSeal?: Pick<WorkerLedgerState, \"terminal\">,\n ) => Effect.Effect<() => void, ThreadImportRejected | ThreadStoreError, ThreadImportReader>;\n}\n\nexport interface MemoryLedgerTransfer {\n readonly snapshot: (threadId: ThreadId) => Effect.Effect<\n Omit<typeof ThreadExportSnapshot.Type, \"deliveries\"> & {\n readonly idle: boolean;\n readonly workerSeal?: Pick<WorkerLedgerState, \"terminal\">;\n },\n ThreadStoreError\n >;\n readonly facts: (\n threadId: ThreadId,\n section: Exclude<TransferSection, \"deliveries\">,\n after: string | undefined,\n ) => Effect.Effect<{ readonly facts: TransferFacts; readonly after: string }, ThreadStoreError>;\n readonly startImport: (\n threadId: ThreadId,\n ) => Effect.Effect<MemoryLedgerImport, ThreadImportRejected | ThreadStoreError>;\n readonly submissions: (threadId: ThreadId) => Stream.Stream<SubmissionSnapshot, ThreadStoreError>;\n readonly admission: (\n threadId: ThreadId,\n submissionId: SubmissionId,\n ) => Effect.Effect<ThreadAdmission | undefined, ThreadStoreError>;\n readonly commands: ThreadImportReader[\"Service\"][\"commands\"];\n}\n\nexport interface MemoryDeliveryImport {\n readonly record: ThreadImportReader[\"Service\"][\"delivery\"];\n readonly stage: (\n page: PreparedImportPage,\n ) => Effect.Effect<void, ThreadImportRejected | ThreadStoreError>;\n readonly has: (messageId: IdempotencyKey) => boolean;\n readonly prepareCommit: () => Effect.Effect<\n () => void,\n ThreadImportRejected | ThreadStoreError,\n ThreadDeliveryImportReader\n >;\n}\n\nexport interface MemoryDeliveryTransfer {\n readonly record: (\n threadId: ThreadId,\n messageId: IdempotencyKey,\n ) => ReturnType<ThreadImportReader[\"Service\"][\"delivery\"]>;\n readonly snapshot: (threadId: ThreadId) => {\n readonly idle: boolean;\n readonly deliveries: number;\n };\n readonly facts: (\n threadId: ThreadId,\n after: string | undefined,\n ) => Effect.Effect<{ readonly facts: TransferFacts; readonly after: string }, ThreadStoreError>;\n readonly startImport: (\n threadId: ThreadId,\n ) => Effect.Effect<MemoryDeliveryImport, ThreadImportRejected | ThreadStoreError>;\n readonly pendingPeerCount: (threadId: ThreadId, limit: number) => number;\n}\n\n/** Native control metadata only; callers already hold the journal's shared gate. */\nexport interface MemoryWorkOwner {\n readonly page: (\n threadId: ThreadId,\n after: string | undefined,\n limit: number,\n ) => Effect.Effect<\n {\n readonly entries: ReadonlyArray<ThreadWorkEntry>;\n readonly after?: string;\n },\n ThreadStoreError\n >;\n readonly threads: (\n request: WorkThreadsRequest,\n ) => Effect.Effect<WorkThreadsPage, ThreadStoreError>;\n}\n\n/** One journal's private mutation boundary, shared with its paired ledger and delivery store. */\nexport class MemoryThreadStoreKernel extends Context.Service<\n MemoryThreadStoreKernel,\n {\n readonly owner: StorageOwner[\"Service\"];\n readonly withMutation: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;\n readonly registerWorkOwner: (\n kind: \"admissions\" | \"deliveries\",\n owner: MemoryWorkOwner,\n ) => Effect.Effect<void>;\n readonly registerDeliveryLookup: (\n exists: (threadId: ThreadId, messageId: IdempotencyKey) => boolean,\n ) => Effect.Effect<void>;\n /** Synchronous publication with the authoritative retained row, including replay membership. */\n readonly checkThreadCapacity: (threadId: ThreadId) => Effect.Effect<void, ThreadStoreError>;\n readonly initializeWork: (threadId: ThreadId) => void;\n readonly retainDelivery: (threadId: ThreadId, messageId: IdempotencyKey) => void;\n readonly registerLedgerTransfer: (transfer: MemoryLedgerTransfer) => Effect.Effect<void>;\n readonly registerDeliveryTransfer: (transfer: MemoryDeliveryTransfer) => Effect.Effect<void>;\n readonly prepareAppend: (\n request: AppendRequest,\n ) => Effect.Effect<PreparedMemoryAppend, ThreadStoreFailure>;\n readonly appendPrepared: (\n prepared: PreparedMemoryAppend,\n ) => Effect.Effect<AppendResult, ThreadStoreFailure>;\n readonly record: (\n threadId: ThreadId,\n recordId: RecordId,\n ) => Effect.Effect<RecordEnvelope | undefined>;\n /** Bounded canonical outcomes for one call; caller holds the shared mutation gate. */\n readonly toolCallResults: (\n threadId: ThreadId,\n runId: RunId,\n toolCallId: ToolCallId,\n ) => Effect.Effect<ReadonlyArray<RecordEnvelope>>;\n readonly tail: (threadId: ThreadId) => Effect.Effect<ThreadTail, ThreadStoreFailure>;\n }\n>()(\"@effect-agent/storage-memory/internal/MemoryThreadStoreKernel\") {}\n","/** Keep only the lowest distinct metadata keys after a cursor while scanning resident indexes. */\nexport const boundedWorkSelection = <A>(\n limit: number,\n key: (value: A) => string,\n after?: string,\n) => {\n const values: Array<A> = [];\n\n return {\n values,\n add: (value: A) => {\n const candidate = key(value);\n\n if (after !== undefined && candidate <= after) return;\n let low = 0;\n let high = values.length;\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n\n if (key(values[middle]) < candidate) low = middle + 1;\n else high = middle;\n }\n if (low >= limit || (low < values.length && key(values[low]) === candidate)) return;\n if (values.length === limit) values.pop();\n values.splice(low, 0, value);\n },\n };\n};\n"],"mappings":";;;AAuHA,IAAa,0BAAb,cAA6C,QAAQ,QAoCnD,CAAC,CAAC,+DAA+D,CAAC,CAAC,CAAC;;;;AC1JtE,MAAa,wBACX,OACA,KACA,UACG;CACH,MAAM,SAAmB,CAAC;CAE1B,OAAO;EACL;EACA,MAAM,UAAa;GACjB,MAAM,YAAY,IAAI,KAAK;GAE3B,IAAI,UAAU,KAAA,KAAa,aAAa,OAAO;GAC/C,IAAI,MAAM;GACV,IAAI,OAAO,OAAO;GAElB,OAAO,MAAM,MAAM;IACjB,MAAM,SAAS,KAAK,OAAO,MAAM,QAAQ,CAAC;IAE1C,IAAI,IAAI,OAAO,OAAO,IAAI,WAAW,MAAM,SAAS;SAC/C,OAAO;GACd;GACA,IAAI,OAAO,SAAU,MAAM,OAAO,UAAU,IAAI,OAAO,IAAI,MAAM,WAAY;GAC7E,IAAI,OAAO,WAAW,OAAO,OAAO,IAAI;GACxC,OAAO,OAAO,KAAK,GAAG,KAAK;EAC7B;CACF;AACF"}
1
+ {"version":3,"file":"WorkSelection-CmqTuws3.mjs","names":[],"sources":["../src/internal/MemoryThreadStoreKernel.ts","../src/internal/WorkSelection.ts"],"sourcesContent":["import type { RunId, SubmissionId, ThreadId, ToolCallId } from \"@yielded/agent/identifiers\";\nimport type { IdempotencyKey } from \"@yielded/agent/receipt\";\nimport type { RecordEnvelope, RecordId } from \"@yielded/agent/records\";\nimport type { StorageOwner } from \"@yielded/agent/run-storage\";\nimport type { SubmissionSnapshot, WorkerLedgerState } from \"@yielded/agent/submission-ledger\";\nimport type {\n PreparedImportPage,\n ThreadImportReader,\n ThreadDeliveryImportReader,\n ThreadImportRejected,\n} from \"@yielded/agent/thread-import\";\nimport type {\n AppendResult,\n AppendRequest,\n ThreadStoreFailure,\n ThreadTail,\n ThreadAdmission,\n ThreadStoreError,\n ThreadExportSnapshot,\n} from \"@yielded/agent/thread-store\";\nimport type { TransferFacts, TransferSection } from \"@yielded/agent/thread-transfer\";\nimport type {\n ThreadWorkEntry,\n WorkThreadsRequest,\n WorkThreadsPage,\n} from \"@yielded/agent/thread-work\";\nimport { Context, type Effect, type Stream } from \"effect\";\n\nexport interface PreparedMemoryAppend {\n readonly request: AppendRequest;\n readonly batchJson: string;\n}\n\n/** Private storage, not a whole-Thread preparation value. Publication callbacks never yield. */\nexport interface MemoryLedgerImport {\n readonly reader: Pick<ThreadImportReader[\"Service\"], \"admission\" | \"runOwner\" | \"commands\">;\n readonly stage: (\n page: PreparedImportPage,\n ) => Effect.Effect<void, ThreadImportRejected | ThreadStoreError>;\n readonly prepareCommit: (\n workerSeal?: Pick<WorkerLedgerState, \"terminal\">,\n ) => Effect.Effect<() => void, ThreadImportRejected | ThreadStoreError, ThreadImportReader>;\n}\n\nexport interface MemoryLedgerTransfer {\n readonly snapshot: (threadId: ThreadId) => Effect.Effect<\n Omit<typeof ThreadExportSnapshot.Type, \"deliveries\"> & {\n readonly idle: boolean;\n readonly workerSeal?: Pick<WorkerLedgerState, \"terminal\">;\n },\n ThreadStoreError\n >;\n readonly facts: (\n threadId: ThreadId,\n section: Exclude<TransferSection, \"deliveries\">,\n after: string | undefined,\n ) => Effect.Effect<{ readonly facts: TransferFacts; readonly after: string }, ThreadStoreError>;\n readonly startImport: (\n threadId: ThreadId,\n ) => Effect.Effect<MemoryLedgerImport, ThreadImportRejected | ThreadStoreError>;\n readonly submissions: (threadId: ThreadId) => Stream.Stream<SubmissionSnapshot, ThreadStoreError>;\n readonly admission: (\n threadId: ThreadId,\n submissionId: SubmissionId,\n ) => Effect.Effect<ThreadAdmission | undefined, ThreadStoreError>;\n readonly commands: ThreadImportReader[\"Service\"][\"commands\"];\n}\n\nexport interface MemoryDeliveryImport {\n readonly record: ThreadImportReader[\"Service\"][\"delivery\"];\n readonly stage: (\n page: PreparedImportPage,\n ) => Effect.Effect<void, ThreadImportRejected | ThreadStoreError>;\n readonly has: (messageId: IdempotencyKey) => boolean;\n readonly prepareCommit: () => Effect.Effect<\n () => void,\n ThreadImportRejected | ThreadStoreError,\n ThreadDeliveryImportReader\n >;\n}\n\nexport interface MemoryDeliveryTransfer {\n readonly record: (\n threadId: ThreadId,\n messageId: IdempotencyKey,\n ) => ReturnType<ThreadImportReader[\"Service\"][\"delivery\"]>;\n readonly snapshot: (threadId: ThreadId) => {\n readonly idle: boolean;\n readonly deliveries: number;\n };\n readonly facts: (\n threadId: ThreadId,\n after: string | undefined,\n ) => Effect.Effect<{ readonly facts: TransferFacts; readonly after: string }, ThreadStoreError>;\n readonly startImport: (\n threadId: ThreadId,\n ) => Effect.Effect<MemoryDeliveryImport, ThreadImportRejected | ThreadStoreError>;\n readonly peerCount: (threadId: ThreadId, nowMillis: number, limit: number) => number;\n}\n\n/** Native control metadata only; callers already hold the journal's shared gate. */\nexport interface MemoryWorkOwner {\n readonly page: (\n threadId: ThreadId,\n after: string | undefined,\n limit: number,\n ) => Effect.Effect<\n {\n readonly entries: ReadonlyArray<ThreadWorkEntry>;\n readonly after?: string;\n },\n ThreadStoreError\n >;\n readonly threads: (\n request: WorkThreadsRequest,\n ) => Effect.Effect<WorkThreadsPage, ThreadStoreError>;\n}\n\n/** One journal's private mutation boundary, shared with its paired ledger and delivery store. */\nexport class MemoryThreadStoreKernel extends Context.Service<\n MemoryThreadStoreKernel,\n {\n readonly owner: StorageOwner[\"Service\"];\n readonly withMutation: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;\n readonly registerWorkOwner: (\n kind: \"admissions\" | \"deliveries\",\n owner: MemoryWorkOwner,\n ) => Effect.Effect<void>;\n readonly registerDeliveryLookup: (\n exists: (threadId: ThreadId, messageId: IdempotencyKey) => boolean,\n ) => Effect.Effect<void>;\n /** Synchronous publication with the authoritative retained row, including replay membership. */\n readonly checkThreadCapacity: (threadId: ThreadId) => Effect.Effect<void, ThreadStoreError>;\n readonly initializeWork: (threadId: ThreadId) => void;\n readonly retainDelivery: (threadId: ThreadId, messageId: IdempotencyKey) => void;\n readonly registerLedgerTransfer: (transfer: MemoryLedgerTransfer) => Effect.Effect<void>;\n readonly registerDeliveryTransfer: (transfer: MemoryDeliveryTransfer) => Effect.Effect<void>;\n readonly prepareAppend: (\n request: AppendRequest,\n ) => Effect.Effect<PreparedMemoryAppend, ThreadStoreFailure>;\n readonly appendPrepared: (\n prepared: PreparedMemoryAppend,\n ) => Effect.Effect<AppendResult, ThreadStoreFailure>;\n readonly record: (\n threadId: ThreadId,\n recordId: RecordId,\n ) => Effect.Effect<RecordEnvelope | undefined>;\n /** Bounded canonical outcomes for one call; caller holds the shared mutation gate. */\n readonly toolCallResults: (\n threadId: ThreadId,\n runId: RunId,\n toolCallId: ToolCallId,\n ) => Effect.Effect<ReadonlyArray<RecordEnvelope>>;\n readonly tail: (threadId: ThreadId) => Effect.Effect<ThreadTail, ThreadStoreFailure>;\n }\n>()(\"@effect-agent/storage-memory/internal/MemoryThreadStoreKernel\") {}\n","/** Keep only the lowest distinct metadata keys after a cursor while scanning resident indexes. */\nexport const boundedWorkSelection = <A>(\n limit: number,\n key: (value: A) => string,\n after?: string,\n) => {\n const values: Array<A> = [];\n\n return {\n values,\n add: (value: A) => {\n const candidate = key(value);\n\n if (after !== undefined && candidate <= after) return;\n let low = 0;\n let high = values.length;\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n\n if (key(values[middle]) < candidate) low = middle + 1;\n else high = middle;\n }\n if (low >= limit || (low < values.length && key(values[low]) === candidate)) return;\n if (values.length === limit) values.pop();\n values.splice(low, 0, value);\n },\n };\n};\n"],"mappings":";;;AAuHA,IAAa,0BAAb,cAA6C,QAAQ,QAoCnD,CAAC,CAAC,+DAA+D,CAAC,CAAC,CAAC;;;;AC1JtE,MAAa,wBACX,OACA,KACA,UACG;CACH,MAAM,SAAmB,CAAC;CAE1B,OAAO;EACL;EACA,MAAM,UAAa;GACjB,MAAM,YAAY,IAAI,KAAK;GAE3B,IAAI,UAAU,KAAA,KAAa,aAAa,OAAO;GAC/C,IAAI,MAAM;GACV,IAAI,OAAO,OAAO;GAElB,OAAO,MAAM,MAAM;IACjB,MAAM,SAAS,KAAK,OAAO,MAAM,QAAQ,CAAC;IAE1C,IAAI,IAAI,OAAO,OAAO,IAAI,WAAW,MAAM,SAAS;SAC/C,OAAO;GACd;GACA,IAAI,OAAO,SAAU,MAAM,OAAO,UAAU,IAAI,OAAO,IAAI,MAAM,WAAY;GAC7E,IAAI,OAAO,WAAW,OAAO,OAAO,IAAI;GACxC,OAAO,OAAO,KAAK,GAAG,KAAK;EAC7B;CACF;AACF"}
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@yielded/agent-storage-memory","version":"0.1.0-beta.168","dependencies":{"@yielded/agent":"0.1.0-beta.168"},"devDependencies":{"@effect/platform-node":"4.0.3","@effect/vitest":"4.0.3","effect":"4.0.3","typescript":"7.0.2","vite":"npm:@voidzero-dev/vite-plus-core@1.1.0","vite-plus":"1.1.0","vitest":"5.0.3"},"peerDependencies":{"effect":"^4.0.3"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./memory-message-delivery-store":{"types":"./dist/MemoryMessageDeliveryStore.d.mts","default":"./dist/MemoryMessageDeliveryStore.mjs"},"./memory-schedule-store":{"types":"./dist/MemoryScheduleStore.d.mts","default":"./dist/MemoryScheduleStore.mjs"},"./memory-semantic-index":{"types":"./dist/MemorySemanticIndex.d.mts","default":"./dist/MemorySemanticIndex.mjs"},"./memory-submission-ledger":{"types":"./dist/MemorySubmissionLedger.d.mts","default":"./dist/MemorySubmissionLedger.mjs"},"./memory-subscription-store":{"types":"./dist/MemorySubscriptionStore.d.mts","default":"./dist/MemorySubscriptionStore.mjs"},"./memory-thread-store":{"types":"./dist/MemoryThreadStore.d.mts","default":"./dist/MemoryThreadStore.mjs"}},"description":"In-memory Thread store and submission ledger reference adapters for Effect Agent tests and conformance suites.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/agent.git","directory":"packages/storage-memory"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
1
+ {"name":"@yielded/agent-storage-memory","version":"0.1.0-beta.170","dependencies":{"@yielded/agent":"0.1.0-beta.170"},"devDependencies":{"@effect/platform-node":"4.0.3","@effect/vitest":"4.0.3","effect":"4.0.3","typescript":"7.0.2","vite":"npm:@voidzero-dev/vite-plus-core@1.1.0","vite-plus":"1.1.0","vitest":"5.0.3"},"peerDependencies":{"effect":"^4.0.3"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./memory-message-delivery-store":{"types":"./dist/MemoryMessageDeliveryStore.d.mts","default":"./dist/MemoryMessageDeliveryStore.mjs"},"./memory-schedule-store":{"types":"./dist/MemoryScheduleStore.d.mts","default":"./dist/MemoryScheduleStore.mjs"},"./memory-semantic-index":{"types":"./dist/MemorySemanticIndex.d.mts","default":"./dist/MemorySemanticIndex.mjs"},"./memory-submission-ledger":{"types":"./dist/MemorySubmissionLedger.d.mts","default":"./dist/MemorySubmissionLedger.mjs"},"./memory-subscription-store":{"types":"./dist/MemorySubscriptionStore.d.mts","default":"./dist/MemorySubscriptionStore.mjs"},"./memory-thread-store":{"types":"./dist/MemoryThreadStore.d.mts","default":"./dist/MemoryThreadStore.mjs"}},"description":"In-memory Thread store and submission ledger reference adapters for Effect Agent tests and conformance suites.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/agent.git","directory":"packages/storage-memory"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
@@ -13,6 +13,7 @@ import {
13
13
  messageDeliveryDeadline,
14
14
  messageDeliveryKeyString,
15
15
  messageDeliveryUsesCapacity,
16
+ peerMessageRetentionDeadline,
16
17
  isWorkerUpdateDelivery,
17
18
  messageDeliveryCapacity,
18
19
  sameMessageDeliveryIdentity,
@@ -86,11 +87,27 @@ const removeKey = (keys: Array<string>, key: string) => {
86
87
  if (keys[at] === key) keys.splice(at, 1);
87
88
  };
88
89
 
90
+ /** First deadline after the request time; retained evidence also supports clock rollback. */
91
+ const deadlinePosition = (deadlines: ReadonlyArray<number>, nowMillis: number) => {
92
+ let low = 0;
93
+ let high = deadlines.length;
94
+
95
+ while (low < high) {
96
+ const mid = Math.floor((low + high) / 2);
97
+
98
+ if (deadlines[mid] <= nowMillis) low = mid + 1;
99
+ else high = mid;
100
+ }
101
+
102
+ return low;
103
+ };
104
+
89
105
  const backend = () => ({
90
106
  records: new Map<string, string>(),
91
107
  work: new Map<string, ThreadWorkEntry>(),
92
108
  ordered: new Map<string, Array<string>>(),
93
109
  pending: new Map<ThreadId, Set<MessageDeliveryKey["messageId"]>>(),
110
+ retainedPeers: new Map<ThreadId, Array<number>>(),
94
111
  counts: new Map<ThreadId, { retained: number; ordinary: number; update: number; peer: number }>(),
95
112
  metadata: new Map<
96
113
  string,
@@ -98,6 +115,7 @@ const backend = () => ({
98
115
  update: boolean;
99
116
  pending: boolean;
100
117
  peer: boolean;
118
+ peerRetentionDeadline: number | null;
101
119
  deadlineKey?: string;
102
120
  importVisit?: "visiting" | "verified";
103
121
  }
@@ -234,6 +252,19 @@ export const memoryMessageDeliveryStoreLayer = (
234
252
  else counts.ordinary++;
235
253
  if (peer) counts.peer++;
236
254
  }
255
+ const retentionDeadline = peerMessageRetentionDeadline(record);
256
+ const previousDeadline = old?.peerRetentionDeadline ?? null;
257
+
258
+ if (previousDeadline !== retentionDeadline) {
259
+ let retained = current.retainedPeers.get(owner);
260
+
261
+ if (retained === undefined) current.retainedPeers.set(owner, (retained = []));
262
+ if (previousDeadline !== null)
263
+ retained.splice(deadlinePosition(retained, previousDeadline) - 1, 1);
264
+ if (retentionDeadline !== null)
265
+ retained.splice(deadlinePosition(retained, retentionDeadline), 0, retentionDeadline);
266
+ if (retained.length === 0) current.retainedPeers.delete(owner);
267
+ }
237
268
  let owned = current.pending.get(owner);
238
269
 
239
270
  if (owned === undefined) current.pending.set(owner, (owned = new Set()));
@@ -283,6 +314,7 @@ export const memoryMessageDeliveryStoreLayer = (
283
314
  update,
284
315
  pending,
285
316
  peer,
317
+ peerRetentionDeadline: retentionDeadline,
286
318
  ...(deadlineKey === undefined ? {} : { deadlineKey }),
287
319
  });
288
320
  if (pending)
@@ -494,8 +526,17 @@ export const memoryMessageDeliveryStoreLayer = (
494
526
  deliveries: counts?.retained ?? 0,
495
527
  };
496
528
  },
497
- pendingPeerCount: (threadId, limit) =>
498
- Math.min(MutableRef.get(state.ref).counts.get(threadId)?.peer ?? 0, limit),
529
+ peerCount: (threadId, nowMillis, limit) => {
530
+ const current = MutableRef.get(state.ref);
531
+ const retained = current.retainedPeers.get(threadId) ?? [];
532
+
533
+ return Math.min(
534
+ (current.counts.get(threadId)?.peer ?? 0) +
535
+ retained.length -
536
+ deadlinePosition(retained, nowMillis),
537
+ limit,
538
+ );
539
+ },
499
540
  facts: Effect.fnUntraced(function* (threadId, after) {
500
541
  const current = MutableRef.get(state.ref);
501
542
  const keys = current.ordered.get(ownerKey(threadId)) ?? [];