@yielded/agent-storage-memory 0.1.0-beta.167 → 0.1.0-beta.169

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.
@@ -57,214 +57,204 @@ const makeScheduleStore = Effect.gen(function* () {
57
57
  const state = yield* Ref.make<MemoryScheduleState>({ records: new Map() });
58
58
  const failpoint = yield* ScheduleFailpoint;
59
59
 
60
- const insert: ScheduleStore["Service"]["insert"] = Effect.fn("MemoryScheduleStore.insert")(
61
- (record, ownerLimit) =>
62
- Effect.gen(function* () {
63
- const encoded = yield* encodeRecord("insert", record);
64
-
65
- yield* failpoint.hit("schedule:insert:before");
66
-
67
- const decision = yield* Effect.uninterruptible(
68
- Ref.modify(
69
- state,
70
- (
71
- current,
72
- ): readonly [
73
- Result.Result<
74
- ScheduleRecord,
75
- ScheduleConflict | ScheduleCapacityError | ScheduleStorageError
76
- >,
77
- MemoryScheduleState,
78
- ] => {
79
- const key = scheduleKeyString(record);
80
- const existingText = current.records.get(key);
81
-
82
- if (existingText !== undefined) {
83
- const decoded = Result.try({
84
- try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(existingText),
85
- catch: () => storageError("insert", "corrupt"),
86
- });
87
-
88
- if (Result.isFailure(decoded)) {
89
- return [Result.fail(decoded.failure), current];
90
- }
91
- if (decoded.success.creationFingerprint === record.creationFingerprint) {
92
- return [Result.succeed(decoded.success), current];
93
- }
94
-
95
- return [
96
- Result.fail(
97
- ScheduleConflict.make({ reason: "creation", key: scheduleKeyOf(record) }),
98
- ),
99
- current,
100
- ];
101
- }
60
+ const insert: ScheduleStore["Service"]["insert"] = Effect.fnUntraced(
61
+ function* (record, ownerLimit) {
62
+ const encoded = yield* encodeRecord("insert", record);
63
+
64
+ yield* failpoint.hit("schedule:insert:before");
65
+
66
+ const decision = yield* Effect.uninterruptible(
67
+ Ref.modify(
68
+ state,
69
+ (
70
+ current,
71
+ ): readonly [
72
+ Result.Result<
73
+ ScheduleRecord,
74
+ ScheduleConflict | ScheduleCapacityError | ScheduleStorageError
75
+ >,
76
+ MemoryScheduleState,
77
+ ] => {
78
+ const key = scheduleKeyString(record);
79
+ const existingText = current.records.get(key);
80
+
81
+ if (existingText !== undefined) {
82
+ const decoded = Result.try({
83
+ try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(existingText),
84
+ catch: () => storageError("insert", "corrupt"),
85
+ });
102
86
 
103
- let ownerCount = 0;
104
-
105
- for (const text of current.records.values()) {
106
- const decoded = Result.try({
107
- try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(text),
108
- catch: () => storageError("insert", "corrupt"),
109
- });
110
-
111
- if (Result.isFailure(decoded)) {
112
- return [Result.fail(decoded.failure), current];
113
- }
114
- if (
115
- sameOwner(decoded.success, record.owner) &&
116
- scheduleUsesCapacity(decoded.success)
117
- )
118
- ownerCount += 1;
87
+ if (Result.isFailure(decoded)) {
88
+ return [Result.fail(decoded.failure), current];
119
89
  }
120
- if (ownerCount >= ownerLimit) {
121
- return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
90
+ if (decoded.success.creationFingerprint === record.creationFingerprint) {
91
+ return [Result.succeed(decoded.success), current];
122
92
  }
123
- const records = new Map(current.records);
124
-
125
- records.set(key, encoded);
126
- const next = { records };
127
-
128
- return [Result.succeed(record), next];
129
- },
130
- ),
131
- );
132
-
133
- const inserted = yield* Effect.fromResult(decision);
134
-
135
- yield* failpoint.hit("schedule:insert:after");
136
-
137
- return yield* decodeRecord("insert", yield* encodeRecord("insert", inserted));
138
- }),
139
- );
140
-
141
- const get: ScheduleStore["Service"]["get"] = Effect.fn("MemoryScheduleStore.get")(
142
- function* (key) {
143
- const decodedKey = yield* decodeInput("get", ScheduleKey, key);
144
- const text = (yield* Ref.get(state)).records.get(scheduleKeyString(decodedKey));
145
93
 
146
- return text === undefined ? null : yield* decodeRecord("get", text);
147
- },
148
- );
94
+ return [
95
+ Result.fail(
96
+ ScheduleConflict.make({ reason: "creation", key: scheduleKeyOf(record) }),
97
+ ),
98
+ current,
99
+ ];
100
+ }
149
101
 
150
- const list: ScheduleStore["Service"]["list"] = Effect.fn("MemoryScheduleStore.list")(
151
- function* (request) {
152
- const decodedRequest = yield* decodeInput("list", SchedulePageRequest, request);
153
- const records: Array<ScheduleRecord> = [];
154
-
155
- for (const text of (yield* Ref.get(state)).records.values()) {
156
- const record = yield* decodeRecord("list", text);
157
-
158
- if (
159
- sameOwner(record, decodedRequest.owner) &&
160
- (decodedRequest.after === undefined ||
161
- compareScheduleNames(record.scheduleId, decodedRequest.after) > 0)
162
- ) {
163
- records.push(record);
164
- }
165
- }
166
- records.sort((left, right) => compareScheduleNames(left.scheduleId, right.scheduleId));
167
- const hasNext = records.length > decodedRequest.limit;
168
- const items = records.slice(0, decodedRequest.limit);
169
-
170
- return {
171
- items,
172
- next: hasNext ? (items.at(-1)?.scheduleId ?? null) : null,
173
- };
174
- },
175
- );
176
-
177
- const change: ScheduleStore["Service"]["change"] = Effect.fn("MemoryScheduleStore.change")(
178
- (key, command, ownerLimit = defaultSchedulingLimits.maxSchedulesPerOwner) =>
179
- Effect.gen(function* () {
180
- const decodedKey = yield* decodeInput("change", ScheduleKey, key);
181
- const decodedCommand = yield* decodeInput("change", ScheduleChange, command);
182
-
183
- yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:before`);
184
-
185
- const decision = yield* Effect.uninterruptible(
186
- Ref.modify(
187
- state,
188
- (
189
- current,
190
- ): readonly [
191
- Result.Result<
192
- ScheduleRecord,
193
- ScheduleConflict | ScheduleStorageError | ScheduleNotFound | ScheduleCapacityError
194
- >,
195
- MemoryScheduleState,
196
- ] => {
197
- const storageKey = scheduleKeyString(decodedKey);
198
- const text = current.records.get(storageKey);
199
-
200
- if (text === undefined) {
201
- return [Result.fail(ScheduleNotFound.make({ key: decodedKey })), current];
202
- }
102
+ let ownerCount = 0;
203
103
 
104
+ for (const text of current.records.values()) {
204
105
  const decoded = Result.try({
205
106
  try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(text),
206
- catch: () => storageError("change", "corrupt"),
107
+ catch: () => storageError("insert", "corrupt"),
207
108
  });
208
109
 
209
110
  if (Result.isFailure(decoded)) {
210
111
  return [Result.fail(decoded.failure), current];
211
112
  }
212
- const applied = applyScheduleChange(decoded.success, decodedCommand);
113
+ if (sameOwner(decoded.success, record.owner) && scheduleUsesCapacity(decoded.success))
114
+ ownerCount += 1;
115
+ }
116
+ if (ownerCount >= ownerLimit) {
117
+ return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
118
+ }
119
+ const records = new Map(current.records);
213
120
 
214
- if (Result.isFailure(applied)) {
215
- return [Result.fail(applied.failure), current];
216
- }
217
- if (applied.success === decoded.success) {
218
- return [Result.succeed(decoded.success), current];
219
- }
220
- if (!scheduleUsesCapacity(decoded.success) && scheduleUsesCapacity(applied.success)) {
221
- let count = 0;
222
-
223
- for (const text of current.records.values()) {
224
- const candidate = Schema.decodeResult(Schema.fromJsonString(ScheduleRecord))(
225
- text,
226
- );
227
-
228
- if (Result.isFailure(candidate))
229
- return [Result.fail(storageError("change", "corrupt")), current];
230
- if (
231
- sameOwner(candidate.success, decodedKey.owner) &&
232
- scheduleUsesCapacity(candidate.success)
233
- )
234
- count += 1;
235
- }
236
- if (count >= ownerLimit)
237
- return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
238
- }
121
+ records.set(key, encoded);
122
+ const next = { records };
239
123
 
240
- const encoded = Result.try({
241
- try: () =>
242
- Schema.encodeSync(Schema.fromJsonString(ScheduleRecord))(applied.success),
243
- catch: () => storageError("change", "corrupt"),
244
- });
124
+ return [Result.succeed(record), next];
125
+ },
126
+ ),
127
+ );
245
128
 
246
- if (Result.isFailure(encoded)) {
247
- return [Result.fail(encoded.failure), current];
248
- }
249
- const records = new Map(current.records);
129
+ const inserted = yield* Effect.fromResult(decision);
250
130
 
251
- records.set(storageKey, encoded.success);
252
- const next = { records };
131
+ yield* failpoint.hit("schedule:insert:after");
253
132
 
254
- return [Result.succeed(applied.success), next];
255
- },
256
- ),
257
- );
133
+ return yield* decodeRecord("insert", yield* encodeRecord("insert", inserted));
134
+ },
135
+ );
258
136
 
259
- const changed = yield* Effect.fromResult(decision);
137
+ const get: ScheduleStore["Service"]["get"] = Effect.fnUntraced(function* (key) {
138
+ const decodedKey = yield* decodeInput("get", ScheduleKey, key);
139
+ const text = (yield* Ref.get(state)).records.get(scheduleKeyString(decodedKey));
260
140
 
261
- yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:after`);
141
+ return text === undefined ? null : yield* decodeRecord("get", text);
142
+ });
262
143
 
263
- return yield* decodeRecord("change", yield* encodeRecord("change", changed));
264
- }),
265
- );
144
+ const list: ScheduleStore["Service"]["list"] = Effect.fnUntraced(function* (request) {
145
+ const decodedRequest = yield* decodeInput("list", SchedulePageRequest, request);
146
+ const records: Array<ScheduleRecord> = [];
147
+
148
+ for (const text of (yield* Ref.get(state)).records.values()) {
149
+ const record = yield* decodeRecord("list", text);
150
+
151
+ if (
152
+ sameOwner(record, decodedRequest.owner) &&
153
+ (decodedRequest.after === undefined ||
154
+ compareScheduleNames(record.scheduleId, decodedRequest.after) > 0)
155
+ ) {
156
+ records.push(record);
157
+ }
158
+ }
159
+ records.sort((left, right) => compareScheduleNames(left.scheduleId, right.scheduleId));
160
+ const hasNext = records.length > decodedRequest.limit;
161
+ const items = records.slice(0, decodedRequest.limit);
162
+
163
+ return {
164
+ items,
165
+ next: hasNext ? (items.at(-1)?.scheduleId ?? null) : null,
166
+ };
167
+ });
168
+
169
+ const change: ScheduleStore["Service"]["change"] = Effect.fnUntraced(function* (
170
+ key,
171
+ command,
172
+ ownerLimit = defaultSchedulingLimits.maxSchedulesPerOwner,
173
+ ) {
174
+ const decodedKey = yield* decodeInput("change", ScheduleKey, key);
175
+ const decodedCommand = yield* decodeInput("change", ScheduleChange, command);
176
+
177
+ yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:before`);
178
+
179
+ const decision = yield* Effect.uninterruptible(
180
+ Ref.modify(
181
+ state,
182
+ (
183
+ current,
184
+ ): readonly [
185
+ Result.Result<
186
+ ScheduleRecord,
187
+ ScheduleConflict | ScheduleStorageError | ScheduleNotFound | ScheduleCapacityError
188
+ >,
189
+ MemoryScheduleState,
190
+ ] => {
191
+ const storageKey = scheduleKeyString(decodedKey);
192
+ const text = current.records.get(storageKey);
193
+
194
+ if (text === undefined) {
195
+ return [Result.fail(ScheduleNotFound.make({ key: decodedKey })), current];
196
+ }
197
+
198
+ const decoded = Result.try({
199
+ try: () => Schema.decodeSync(Schema.fromJsonString(ScheduleRecord))(text),
200
+ catch: () => storageError("change", "corrupt"),
201
+ });
202
+
203
+ if (Result.isFailure(decoded)) {
204
+ return [Result.fail(decoded.failure), current];
205
+ }
206
+ const applied = applyScheduleChange(decoded.success, decodedCommand);
207
+
208
+ if (Result.isFailure(applied)) {
209
+ return [Result.fail(applied.failure), current];
210
+ }
211
+ if (applied.success === decoded.success) {
212
+ return [Result.succeed(decoded.success), current];
213
+ }
214
+ if (!scheduleUsesCapacity(decoded.success) && scheduleUsesCapacity(applied.success)) {
215
+ let count = 0;
216
+
217
+ for (const text of current.records.values()) {
218
+ const candidate = Schema.decodeResult(Schema.fromJsonString(ScheduleRecord))(text);
219
+
220
+ if (Result.isFailure(candidate))
221
+ return [Result.fail(storageError("change", "corrupt")), current];
222
+ if (
223
+ sameOwner(candidate.success, decodedKey.owner) &&
224
+ scheduleUsesCapacity(candidate.success)
225
+ )
226
+ count += 1;
227
+ }
228
+ if (count >= ownerLimit)
229
+ return [Result.fail(ScheduleCapacityError.make({ limit: ownerLimit })), current];
230
+ }
231
+
232
+ const encoded = Result.try({
233
+ try: () => Schema.encodeSync(Schema.fromJsonString(ScheduleRecord))(applied.success),
234
+ catch: () => storageError("change", "corrupt"),
235
+ });
236
+
237
+ if (Result.isFailure(encoded)) {
238
+ return [Result.fail(encoded.failure), current];
239
+ }
240
+ const records = new Map(current.records);
241
+
242
+ records.set(storageKey, encoded.success);
243
+ const next = { records };
244
+
245
+ return [Result.succeed(applied.success), next];
246
+ },
247
+ ),
248
+ );
249
+
250
+ const changed = yield* Effect.fromResult(decision);
251
+
252
+ yield* failpoint.hit(`schedule:${decodedCommand._tag.toLowerCase()}:after`);
253
+
254
+ return yield* decodeRecord("change", yield* encodeRecord("change", changed));
255
+ });
266
256
 
267
- const due: ScheduleStore["Service"]["due"] = Effect.fn("MemoryScheduleStore.due")(
257
+ const due: ScheduleStore["Service"]["due"] = Effect.fnUntraced(
268
258
  function* (nowMillis, limit, owner, after) {
269
259
  const decodedOwner =
270
260
  owner === undefined ? undefined : yield* decodeInput("due", ScheduleOwner, owner);
@@ -299,25 +289,25 @@ const makeScheduleStore = Effect.gen(function* () {
299
289
  },
300
290
  );
301
291
 
302
- const nextDeadline: ScheduleStore["Service"]["nextDeadline"] = Effect.fn(
303
- "MemoryScheduleStore.nextDeadline",
304
- )(function* (owner) {
305
- const decodedOwner =
306
- owner === undefined ? undefined : yield* decodeInput("nextDeadline", ScheduleOwner, owner);
292
+ const nextDeadline: ScheduleStore["Service"]["nextDeadline"] = Effect.fnUntraced(
293
+ function* (owner) {
294
+ const decodedOwner =
295
+ owner === undefined ? undefined : yield* decodeInput("nextDeadline", ScheduleOwner, owner);
307
296
 
308
- let earliest: number | null = null;
297
+ let earliest: number | null = null;
309
298
 
310
- for (const text of (yield* Ref.get(state)).records.values()) {
311
- const record = yield* decodeRecord("nextDeadline", text);
299
+ for (const text of (yield* Ref.get(state)).records.values()) {
300
+ const record = yield* decodeRecord("nextDeadline", text);
312
301
 
313
- if (decodedOwner !== undefined && !sameOwner(record, decodedOwner)) continue;
314
- const deadline = scheduleDeadline(record);
302
+ if (decodedOwner !== undefined && !sameOwner(record, decodedOwner)) continue;
303
+ const deadline = scheduleDeadline(record);
315
304
 
316
- if (deadline !== null && (earliest === null || deadline < earliest)) earliest = deadline;
317
- }
305
+ if (deadline !== null && (earliest === null || deadline < earliest)) earliest = deadline;
306
+ }
318
307
 
319
- return earliest;
320
- });
308
+ return earliest;
309
+ },
310
+ );
321
311
 
322
312
  return ScheduleStore.of({ insert, get, list, change, due, nextDeadline });
323
313
  });
@@ -60,16 +60,15 @@ const keyString = (key: MemoryKey): string => JSON.stringify([key.namespace.addr
60
60
  const sourceIdentityBytes = (source: MemoryIndexSource): number =>
61
61
  Hex.encode(JSON.stringify(source)).length / 2;
62
62
 
63
- const decodeBoundary = Effect.fn("InMemorySemanticIndex.decodeBoundary")(function* <A, I>(
63
+ const decodeBoundary = <A, I>(
64
64
  schema: Schema.Codec<A, I, never>,
65
65
  value: unknown,
66
66
  operation: string,
67
- ): Effect.fn.Return<A, MemoryIndexError> {
68
- return yield* Schema.decodeUnknownEffect(schema)(value).pipe(
67
+ ): Effect.Effect<A, MemoryIndexError> =>
68
+ Schema.decodeUnknownEffect(schema)(value).pipe(
69
69
  Effect.flatMap((decoded) => Schema.encodeEffect(schema)(decoded).pipe(Effect.as(decoded))),
70
70
  Effect.mapError(() => error(operation, "invalid-input")),
71
71
  );
72
- });
73
72
 
74
73
  const freezeSource = (source: MemoryIndexSource): MemoryIndexSource =>
75
74
  Object.freeze(
@@ -107,7 +106,7 @@ const squaredNorm = (vector: ReadonlyArray<number>): number | null => {
107
106
  const validVector = (vector: ReadonlyArray<number>, profile: SemanticMemoryProfile): boolean =>
108
107
  vector.length === profile.dimensions && squaredNorm(vector) !== null;
109
108
 
110
- const validateChunks = Effect.fn("InMemorySemanticIndex.validateChunks")(function* (
109
+ const validateChunks = Effect.fnUntraced(function* (
111
110
  chunks: ReadonlyArray<SemanticMemoryChunk>,
112
111
  profile: SemanticMemoryProfile,
113
112
  operation: string,
@@ -151,7 +150,7 @@ const cosine = (left: ReadonlyArray<number>, right: ReadonlyArray<number>): numb
151
150
  const compareText = (left: string, right: string): number =>
152
151
  left < right ? -1 : left > right ? 1 : 0;
153
152
 
154
- const makeIndex = Effect.fn("InMemorySemanticIndex.make")(function* (
153
+ const makeIndex = Effect.fnUntraced(function* (
155
154
  rawProfile: SemanticMemoryProfile,
156
155
  rawCapacity: InMemorySemanticIndexCapacity,
157
156
  ) {
@@ -181,110 +180,110 @@ const makeIndex = Effect.fn("InMemorySemanticIndex.make")(function* (
181
180
  Ref.set(data, { closed: true, entries: new Map(), sourceBytes: 0 }),
182
181
  );
183
182
 
184
- const ensureOpen = Effect.fn("InMemorySemanticIndex.ensureOpen")(function* (operation: string) {
183
+ const ensureOpen = Effect.fnUntraced(function* (operation: string) {
185
184
  if ((yield* Ref.get(data)).closed) return yield* error(operation, "unavailable");
186
185
  });
187
186
 
188
- const replace: SemanticMemoryIndex["Service"]["replace"] = Effect.fn(
189
- "InMemorySemanticIndex.replace",
190
- )(function* (rawRequest) {
191
- const operation = "replace semantic memory source";
192
-
193
- yield* ensureOpen(operation);
194
- const request = yield* decodeBoundary(MemoryIndexReplacement.Wire, rawRequest, operation);
195
- const source = freezeSource(request.source);
196
- const sourceBytes = sourceIdentityBytes(source);
197
- const chunks = Object.freeze(request.chunks.map(freezeChunk));
198
-
199
- if (source.source.id !== source.key.id) return yield* error(operation, "invalid-input");
200
- if (!sameProfile(request.profile, profile)) return yield* error(operation, "incompatible");
201
- yield* validateChunks(chunks, profile, operation);
202
- const indexedAt = yield* Clock.currentTimeMillis;
203
-
204
- const failure = yield* Ref.modify(
205
- data,
206
- (current): readonly [MemoryIndexError | undefined, IndexData] => {
207
- if (current.closed) return [error(operation, "unavailable"), current];
208
- const id = keyString(source.key);
209
- const existing = current.entries.get(id);
210
-
211
- if (
212
- existing !== undefined &&
213
- (existing._tag === "Withdrawn" || sourceIsFenced(source, existing))
214
- ) {
215
- return [error(operation, "fenced"), current];
216
- }
217
- if (existing === undefined && current.entries.size >= capacity.maxSources) {
218
- return [error(operation, "budget"), current];
219
- }
220
- const nextSourceBytes = current.sourceBytes - (existing?.sourceBytes ?? 0) + sourceBytes;
221
-
222
- if (nextSourceBytes > maxSourceBytes) return [error(operation, "budget"), current];
223
- let count = chunks.length;
224
-
225
- for (const [entryId, entry] of current.entries) {
226
- if (entryId !== id && entry._tag === "Indexed") count += entry.chunks.length;
227
- }
228
- if (count > capacity.maxChunks) return [error(operation, "budget"), current];
229
- const entries = new Map(current.entries);
230
-
231
- entries.set(id, { _tag: "Indexed", source, sourceBytes, chunks, indexedAt });
232
-
233
- return [undefined, { ...current, entries, sourceBytes: nextSourceBytes }];
234
- },
235
- );
236
-
237
- if (failure !== undefined) return yield* failure;
238
- });
239
-
240
- const withdraw: SemanticMemoryIndex["Service"]["withdraw"] = Effect.fn(
241
- "InMemorySemanticIndex.withdraw",
242
- )(function* (rawSource) {
243
- const operation = "withdraw semantic memory source";
187
+ const replace: SemanticMemoryIndex["Service"]["replace"] = Effect.fnUntraced(
188
+ function* (rawRequest) {
189
+ const operation = "replace semantic memory source";
190
+
191
+ yield* ensureOpen(operation);
192
+ const request = yield* decodeBoundary(MemoryIndexReplacement.Wire, rawRequest, operation);
193
+ const source = freezeSource(request.source);
194
+ const sourceBytes = sourceIdentityBytes(source);
195
+ const chunks = Object.freeze(request.chunks.map(freezeChunk));
196
+
197
+ if (source.source.id !== source.key.id) return yield* error(operation, "invalid-input");
198
+ if (!sameProfile(request.profile, profile)) return yield* error(operation, "incompatible");
199
+ yield* validateChunks(chunks, profile, operation);
200
+ const indexedAt = yield* Clock.currentTimeMillis;
201
+
202
+ const failure = yield* Ref.modify(
203
+ data,
204
+ (current): readonly [MemoryIndexError | undefined, IndexData] => {
205
+ if (current.closed) return [error(operation, "unavailable"), current];
206
+ const id = keyString(source.key);
207
+ const existing = current.entries.get(id);
208
+
209
+ if (
210
+ existing !== undefined &&
211
+ (existing._tag === "Withdrawn" || sourceIsFenced(source, existing))
212
+ ) {
213
+ return [error(operation, "fenced"), current];
214
+ }
215
+ if (existing === undefined && current.entries.size >= capacity.maxSources) {
216
+ return [error(operation, "budget"), current];
217
+ }
218
+ const nextSourceBytes = current.sourceBytes - (existing?.sourceBytes ?? 0) + sourceBytes;
244
219
 
245
- yield* ensureOpen(operation);
220
+ if (nextSourceBytes > maxSourceBytes) return [error(operation, "budget"), current];
221
+ let count = chunks.length;
246
222
 
247
- const source = freezeSource(
248
- yield* decodeBoundary(MemoryIndexSource.Wire, rawSource, operation),
249
- );
223
+ for (const [entryId, entry] of current.entries) {
224
+ if (entryId !== id && entry._tag === "Indexed") count += entry.chunks.length;
225
+ }
226
+ if (count > capacity.maxChunks) return [error(operation, "budget"), current];
227
+ const entries = new Map(current.entries);
250
228
 
251
- const sourceBytes = sourceIdentityBytes(source);
229
+ entries.set(id, { _tag: "Indexed", source, sourceBytes, chunks, indexedAt });
252
230
 
253
- if (source.source.id !== source.key.id) return yield* error(operation, "invalid-input");
231
+ return [undefined, { ...current, entries, sourceBytes: nextSourceBytes }];
232
+ },
233
+ );
254
234
 
255
- const failure = yield* Ref.modify(
256
- data,
257
- (current): readonly [MemoryIndexError | undefined, IndexData] => {
258
- if (current.closed) return [error(operation, "unavailable"), current];
259
- const id = keyString(source.key);
260
- const existing = current.entries.get(id);
235
+ if (failure !== undefined) return yield* failure;
236
+ },
237
+ );
261
238
 
262
- if (existing !== undefined) {
263
- if (existing._tag === "Withdrawn") {
264
- return [
265
- sameSource(source, existing.source) ? undefined : error(operation, "fenced"),
266
- current,
267
- ];
239
+ const withdraw: SemanticMemoryIndex["Service"]["withdraw"] = Effect.fnUntraced(
240
+ function* (rawSource) {
241
+ const operation = "withdraw semantic memory source";
242
+
243
+ yield* ensureOpen(operation);
244
+
245
+ const source = freezeSource(
246
+ yield* decodeBoundary(MemoryIndexSource.Wire, rawSource, operation),
247
+ );
248
+
249
+ const sourceBytes = sourceIdentityBytes(source);
250
+
251
+ if (source.source.id !== source.key.id) return yield* error(operation, "invalid-input");
252
+
253
+ const failure = yield* Ref.modify(
254
+ data,
255
+ (current): readonly [MemoryIndexError | undefined, IndexData] => {
256
+ if (current.closed) return [error(operation, "unavailable"), current];
257
+ const id = keyString(source.key);
258
+ const existing = current.entries.get(id);
259
+
260
+ if (existing !== undefined) {
261
+ if (existing._tag === "Withdrawn") {
262
+ return [
263
+ sameSource(source, existing.source) ? undefined : error(operation, "fenced"),
264
+ current,
265
+ ];
266
+ }
267
+ if (sourceIsFenced(source, existing)) return [error(operation, "fenced"), current];
268
+ } else if (current.entries.size >= capacity.maxSources) {
269
+ return [error(operation, "budget"), current];
268
270
  }
269
- if (sourceIsFenced(source, existing)) return [error(operation, "fenced"), current];
270
- } else if (current.entries.size >= capacity.maxSources) {
271
- return [error(operation, "budget"), current];
272
- }
273
- const nextSourceBytes = current.sourceBytes - (existing?.sourceBytes ?? 0) + sourceBytes;
271
+ const nextSourceBytes = current.sourceBytes - (existing?.sourceBytes ?? 0) + sourceBytes;
274
272
 
275
- if (nextSourceBytes > maxSourceBytes) return [error(operation, "budget"), current];
276
- const entries = new Map(current.entries);
273
+ if (nextSourceBytes > maxSourceBytes) return [error(operation, "budget"), current];
274
+ const entries = new Map(current.entries);
277
275
 
278
- entries.set(id, { _tag: "Withdrawn", source, sourceBytes });
276
+ entries.set(id, { _tag: "Withdrawn", source, sourceBytes });
279
277
 
280
- return [undefined, { ...current, entries, sourceBytes: nextSourceBytes }];
281
- },
282
- );
278
+ return [undefined, { ...current, entries, sourceBytes: nextSourceBytes }];
279
+ },
280
+ );
283
281
 
284
- if (failure !== undefined) return yield* failure;
285
- });
282
+ if (failure !== undefined) return yield* failure;
283
+ },
284
+ );
286
285
 
287
- const search = Effect.fn("InMemorySemanticIndex.search")(function* (rawQuery: MemoryIndexQuery) {
286
+ const search = Effect.fnUntraced(function* (rawQuery: MemoryIndexQuery) {
288
287
  const operation = "search semantic memory index";
289
288
 
290
289
  yield* ensureOpen(operation);