@joshuaswarren/openclaw-engram 9.1.23 → 9.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -409,7 +409,7 @@ openclaw engram access http-serve --token "$OPENCLAW_ENGRAM_ACCESS_TOKEN"
409
409
 
410
410
  Key endpoints: `GET /engram/v1/health`, `POST /engram/v1/recall`, `POST /engram/v1/memories`, `GET /engram/v1/entities/:name`, and more. Full reference in [API docs](docs/api.md).
411
411
 
412
- The HTTP server also hosts a lightweight operator UI at `http://127.0.0.1:4318/engram/ui/` for memory browsing, recall inspection, governance review, and entity exploration.
412
+ The HTTP server also hosts a lightweight operator UI at `http://127.0.0.1:4318/engram/ui/` for memory browsing, recall inspection, governance review, trust-zone promotion, and entity exploration.
413
413
 
414
414
  ### MCP Tools
415
415
 
@@ -473,8 +473,29 @@ openclaw engram semantic-consolidate --dry-run # Preview without changes
473
473
  # Access layer
474
474
  openclaw engram access http-serve --token "$TOKEN" # Start HTTP API
475
475
  openclaw engram access mcp-serve # Start stdio MCP server
476
+
477
+ # Trust-zone demos
478
+ openclaw engram trust-zone-demo-seed --dry-run # Preview the opt-in buyer demo dataset
479
+ openclaw engram trust-zone-demo-seed # Explicitly seed the demo dataset
480
+ openclaw engram trust-zone-promote --record-id <id> --target-zone working --reason "Operator review"
476
481
  ```
477
482
 
483
+ ### Trust-zone demo workflow
484
+
485
+ Trust zones now ship with a dedicated admin-console view plus an explicit demo seeding path for buyer-facing walkthroughs.
486
+
487
+ - **Never automatic** — Engram does not seed sample trust-zone records on install, startup, or feature enablement.
488
+ - **Explicit only** — demo records appear only after you run `openclaw engram trust-zone-demo-seed` or trigger the matching admin-console action.
489
+ - **Buyer-friendly story** — the trust-zone view surfaces provenance strength, promotion readiness, corroboration requirements, and operator promotion actions in one place.
490
+
491
+ The seeded scenario is `enterprise-buyer-v1`, which creates a small, opinionated dataset covering:
492
+
493
+ - quarantine records that are ready for review
494
+ - working records that are blocked on missing provenance
495
+ - working records that still need corroboration
496
+ - working records with independent corroboration support
497
+ - a trusted operator policy record
498
+
478
499
  See the [full CLI reference](docs/api.md#cli-commands) for all commands.
479
500
 
480
501
  ## Configuration
@@ -3,10 +3,10 @@ import {
3
3
  EngramAccessService,
4
4
  Orchestrator,
5
5
  parseConfig
6
- } from "./chunk-S6X3YWYF.js";
7
- import "./chunk-PIQWOQEX.js";
6
+ } from "./chunk-JUQ752QV.js";
7
+ import "./chunk-MNBCF5B2.js";
8
8
  import "./chunk-IMMYYNXG.js";
9
- import "./chunk-Y6DMH5LN.js";
9
+ import "./chunk-IADQJS2J.js";
10
10
  import "./chunk-6KX4XLQJ.js";
11
11
  import "./chunk-4722S3A6.js";
12
12
  import "./chunk-MKM2BCQH.js";
@@ -5,6 +5,11 @@ const browserState = {
5
5
  offset: 0,
6
6
  total: 0,
7
7
  };
8
+ const trustZoneState = {
9
+ limit: 12,
10
+ offset: 0,
11
+ total: 0,
12
+ };
8
13
 
9
14
  function $(id) {
10
15
  return document.getElementById(id);
@@ -109,12 +114,43 @@ function readMemoryPageSize() {
109
114
  return Number.parseInt($("memoryPageSize")?.value || String(browserState.limit || 25), 10) || 25;
110
115
  }
111
116
 
117
+ function readTrustZonePageSize() {
118
+ return Number.parseInt($("trustZonePageSize")?.value || String(trustZoneState.limit || 12), 10) || 12;
119
+ }
120
+
112
121
  function stepMemoryPage(direction) {
113
122
  const pageSize = readMemoryPageSize();
114
123
  browserState.limit = pageSize;
115
124
  browserState.offset = Math.max(0, browserState.offset + direction * pageSize);
116
125
  }
117
126
 
127
+ function syncTrustZoneControls() {
128
+ const prevButton = $("trustZonePrevButton");
129
+ const nextButton = $("trustZoneNextButton");
130
+ if (prevButton) prevButton.disabled = trustZoneState.offset <= 0;
131
+ if (nextButton) nextButton.disabled = trustZoneState.offset + trustZoneState.limit >= trustZoneState.total;
132
+
133
+ const pageStatus = $("trustZonePageStatus");
134
+ if (!pageStatus) return;
135
+ if (trustZoneState.total === 0) {
136
+ pageStatus.textContent = "No results";
137
+ return;
138
+ }
139
+ const pageOffset = Math.min(
140
+ trustZoneState.offset,
141
+ Math.max(0, trustZoneState.total - 1),
142
+ );
143
+ const start = pageOffset + 1;
144
+ const end = Math.min(pageOffset + trustZoneState.limit, trustZoneState.total);
145
+ pageStatus.textContent = `${start}-${end} of ${trustZoneState.total}`;
146
+ }
147
+
148
+ function stepTrustZonePage(direction) {
149
+ const pageSize = readTrustZonePageSize();
150
+ trustZoneState.limit = pageSize;
151
+ trustZoneState.offset = Math.max(0, trustZoneState.offset + direction * pageSize);
152
+ }
153
+
118
154
  function renderMemoryList(memories) {
119
155
  const list = $("memoryList");
120
156
  if (!list) return;
@@ -254,6 +290,86 @@ function renderEntityList(entities) {
254
290
  });
255
291
  }
256
292
 
293
+ function renderTrustZoneList(records) {
294
+ const list = $("trustZoneList");
295
+ if (!list) return;
296
+ if (!Array.isArray(records) || records.length === 0) {
297
+ renderEmptyState(list, "No trust-zone records matched.");
298
+ return;
299
+ }
300
+ clearChildren(list);
301
+ records.forEach((record) => {
302
+ const article = createItem();
303
+ const meta = document.createElement("div");
304
+ meta.className = "meta";
305
+ appendPill(meta, record.zone);
306
+ appendPill(meta, record.kind);
307
+ appendPill(meta, record.sourceClass);
308
+ appendPill(meta, record.anchored ? "anchored" : "unanchored");
309
+ if (record.trustScore) {
310
+ appendPill(meta, `trust ${record.trustScore.total} (${record.trustScore.band})`);
311
+ }
312
+ article.appendChild(meta);
313
+
314
+ const heading = document.createElement("h3");
315
+ heading.style.marginTop = "10px";
316
+ heading.textContent = record.recordId;
317
+ article.appendChild(heading);
318
+
319
+ const pathText = document.createElement("div");
320
+ pathText.className = "status";
321
+ pathText.textContent = `${record.recordedAt} · ${record.filePath}`;
322
+ article.appendChild(pathText);
323
+
324
+ const preview = document.createElement("p");
325
+ preview.textContent = record.summary;
326
+ article.appendChild(preview);
327
+
328
+ const readiness = document.createElement("div");
329
+ readiness.className = "status";
330
+ if (record.nextPromotionTarget) {
331
+ readiness.textContent = record.nextPromotionAllowed
332
+ ? `Ready for promotion to ${record.nextPromotionTarget}.`
333
+ : `Blocked on ${record.nextPromotionTarget}: ${(record.nextPromotionReasons || []).join("; ") || "operator review required"}`;
334
+ } else {
335
+ readiness.textContent = "No further promotion path.";
336
+ }
337
+ article.appendChild(readiness);
338
+
339
+ const toolbar = document.createElement("div");
340
+ toolbar.className = "toolbar";
341
+ toolbar.style.marginTop = "12px";
342
+
343
+ const inspectButton = document.createElement("button");
344
+ inspectButton.className = "secondary";
345
+ inspectButton.textContent = "Inspect";
346
+ inspectButton.addEventListener("click", () => {
347
+ $("trustZoneDetail").textContent = JSON.stringify(record, null, 2);
348
+ setStatus("trustZoneDetailStatus", `Loaded ${record.recordId}.`, "ok");
349
+ });
350
+ toolbar.appendChild(inspectButton);
351
+
352
+ if (record.nextPromotionTarget) {
353
+ const previewButton = document.createElement("button");
354
+ previewButton.className = "secondary";
355
+ previewButton.textContent = `Preview → ${record.nextPromotionTarget}`;
356
+ previewButton.addEventListener("click", () => void promoteTrustZone(record.recordId, record.nextPromotionTarget, true));
357
+ toolbar.appendChild(previewButton);
358
+ }
359
+
360
+ if (record.nextPromotionTarget && record.nextPromotionAllowed) {
361
+ const promoteButton = document.createElement("button");
362
+ promoteButton.className = "accent";
363
+ promoteButton.textContent = `Promote → ${record.nextPromotionTarget}`;
364
+ promoteButton.addEventListener("click", () => void promoteTrustZone(record.recordId, record.nextPromotionTarget, false));
365
+ toolbar.appendChild(promoteButton);
366
+ }
367
+
368
+ article.appendChild(toolbar);
369
+ list.appendChild(article);
370
+ });
371
+ }
372
+
257
373
  function renderQuality(response) {
258
374
  const summary = $("qualitySummary");
259
375
  if (!summary) return;
@@ -418,6 +534,99 @@ async function loadMaintenance() {
418
534
  setStatus("maintenanceStatus", "Maintenance summary loaded.", "ok");
419
535
  }
420
536
 
537
+ async function loadTrustZones(resetOffset = false) {
538
+ if (resetOffset) trustZoneState.offset = 0;
539
+ trustZoneState.limit = readTrustZonePageSize();
540
+ setStatus("trustZoneStatus", "Loading trust-zone state...");
541
+ const params = new URLSearchParams();
542
+ const query = $("trustZoneQuery")?.value?.trim();
543
+ const zone = $("trustZoneZone")?.value?.trim();
544
+ const sourceClass = $("trustZoneSourceClass")?.value?.trim();
545
+ if (query) params.set("q", query);
546
+ if (zone) params.set("zone", zone);
547
+ if (sourceClass) params.set("sourceClass", sourceClass);
548
+ params.set("limit", String(trustZoneState.limit));
549
+ params.set("offset", String(trustZoneState.offset));
550
+
551
+ const [statusResponse, browseResponse] = await Promise.all([
552
+ fetchJson("/engram/v1/trust-zones/status"),
553
+ fetchJson(`/engram/v1/trust-zones/records?${params.toString()}`),
554
+ ]);
555
+ trustZoneState.total = browseResponse.total || 0;
556
+ const maxOffset = trustZoneState.total > 0
557
+ ? Math.floor((trustZoneState.total - 1) / trustZoneState.limit) * trustZoneState.limit
558
+ : 0;
559
+ if (!resetOffset && trustZoneState.offset > maxOffset) {
560
+ trustZoneState.offset = maxOffset;
561
+ return loadTrustZones(false);
562
+ }
563
+
564
+ renderTrustZoneList(browseResponse.records);
565
+ syncTrustZoneControls();
566
+ const byZone = statusResponse?.status?.records?.byZone || {};
567
+ const zoneSummary = ["quarantine", "working", "trusted"]
568
+ .filter((name) => typeof byZone[name] === "number")
569
+ .map((name) => `${name} ${byZone[name]}`)
570
+ .join(" · ");
571
+ setStatus(
572
+ "trustZoneStatus",
573
+ `Loaded ${browseResponse.count} of ${browseResponse.total} trust-zone records.${zoneSummary ? ` ${zoneSummary}.` : ""}`,
574
+ "ok",
575
+ );
576
+ }
577
+
578
+ async function promoteTrustZone(recordId, targetZone, dryRun) {
579
+ if (!recordId || !targetZone) return;
580
+ setStatus("trustZoneDetailStatus", `${dryRun ? "Previewing" : "Applying"} ${targetZone} promotion for ${recordId}...`);
581
+ const response = await fetchJson("/engram/v1/trust-zones/promote", {
582
+ method: "POST",
583
+ body: JSON.stringify({
584
+ recordId,
585
+ targetZone,
586
+ promotionReason: dryRun
587
+ ? `Previewed in Engram admin console for ${recordId}.`
588
+ : `Promoted in Engram admin console for ${recordId}.`,
589
+ dryRun,
590
+ }),
591
+ });
592
+ $("trustZoneSeedResult").textContent = JSON.stringify(response, null, 2);
593
+ $("trustZoneDetail").textContent = JSON.stringify(response.record, null, 2);
594
+ await loadTrustZones(false);
595
+ setStatus(
596
+ "trustZoneDetailStatus",
597
+ dryRun ? `Previewed ${targetZone} promotion for ${recordId}.` : `Applied ${targetZone} promotion for ${recordId}.`,
598
+ "ok",
599
+ );
600
+ }
601
+
602
+ async function seedTrustZoneDemo(dryRun) {
603
+ if (!dryRun && typeof window.confirm === "function") {
604
+ const confirmed = window.confirm(
605
+ "Seed the explicit trust-zone demo dataset into the current namespace? This is opt-in demo data for buyer-facing walkthroughs.",
606
+ );
607
+ if (!confirmed) return;
608
+ }
609
+ setStatus("trustZoneStatus", dryRun ? "Previewing trust-zone demo seed..." : "Seeding trust-zone demo dataset...");
610
+ const response = await fetchJson("/engram/v1/trust-zones/demo-seed", {
611
+ method: "POST",
612
+ body: JSON.stringify({
613
+ scenario: "enterprise-buyer-v1",
614
+ dryRun,
615
+ }),
616
+ });
617
+ $("trustZoneSeedResult").textContent = JSON.stringify(response, null, 2);
618
+ if (!dryRun) {
619
+ await loadTrustZones(true);
620
+ }
621
+ setStatus(
622
+ "trustZoneStatus",
623
+ dryRun
624
+ ? `Previewed ${response.records.length} trust-zone demo records.`
625
+ : `Seeded ${response.recordsWritten} trust-zone demo records into ${response.namespace}.`,
626
+ "ok",
627
+ );
628
+ }
629
+
421
630
  async function connectAndBootstrap() {
422
631
  const input = $("tokenInput");
423
632
  const token = input?.value?.trim() || readToken();
@@ -433,6 +642,7 @@ async function connectAndBootstrap() {
433
642
  setStatus("authStatus", "Connected to Engram access API.", "ok");
434
643
  await Promise.allSettled([
435
644
  loadMemoryBrowser(true),
645
+ loadTrustZones(true),
436
646
  loadReviewQueue(),
437
647
  loadEntities(),
438
648
  loadQuality(),
@@ -485,6 +695,17 @@ function bootstrap() {
485
695
  void loadMemoryBrowser(false);
486
696
  });
487
697
  $("runRecallButton")?.addEventListener("click", () => void runRecallDebugger());
698
+ $("refreshTrustZonesButton")?.addEventListener("click", () => void loadTrustZones(true));
699
+ $("trustZonePrevButton")?.addEventListener("click", () => {
700
+ stepTrustZonePage(-1);
701
+ void loadTrustZones(false);
702
+ });
703
+ $("trustZoneNextButton")?.addEventListener("click", () => {
704
+ stepTrustZonePage(1);
705
+ void loadTrustZones(false);
706
+ });
707
+ $("previewTrustZoneSeedButton")?.addEventListener("click", () => void seedTrustZoneDemo(true));
708
+ $("seedTrustZoneDemoButton")?.addEventListener("click", () => void seedTrustZoneDemo(false));
488
709
  $("refreshQueueButton")?.addEventListener("click", () => void loadReviewQueue());
489
710
  $("searchEntitiesButton")?.addEventListener("click", () => void loadEntities());
490
711
  $("copyMemoryPathButton")?.addEventListener("click", copyMemoryPath);
@@ -493,6 +714,7 @@ function bootstrap() {
493
714
  void connectAndBootstrap();
494
715
  } else {
495
716
  syncBrowserControls();
717
+ syncTrustZoneControls();
496
718
  }
497
719
  }
498
720
 
@@ -194,6 +194,15 @@
194
194
  font-size: 0.84rem;
195
195
  color: var(--muted);
196
196
  }
197
+ .callout {
198
+ border: 1px dashed var(--line);
199
+ border-radius: 14px;
200
+ padding: 12px 14px;
201
+ background: rgba(255, 255, 255, 0.62);
202
+ color: var(--muted);
203
+ font-size: 0.88rem;
204
+ line-height: 1.45;
205
+ }
197
206
  .status.ok {
198
207
  color: var(--ok);
199
208
  }
@@ -360,6 +369,69 @@
360
369
  </section>
361
370
  </div>
362
371
 
372
+ <div class="grid" style="margin-top: 16px;">
373
+ <section class="card">
374
+ <h2>Trust Zones</h2>
375
+ <div class="callout">
376
+ Demo data is never seeded automatically. Use the explicit seed action below only when you want a buyer-facing trust-zone walkthrough dataset.
377
+ </div>
378
+ <div class="toolbar" style="margin-top: 12px;">
379
+ <label style="flex: 1;">
380
+ Query
381
+ <input id="trustZoneQuery" type="text" placeholder="Search summaries, tags, metadata, or entity refs" />
382
+ </label>
383
+ <label>
384
+ Zone
385
+ <select id="trustZoneZone">
386
+ <option value="">All</option>
387
+ <option value="quarantine">quarantine</option>
388
+ <option value="working">working</option>
389
+ <option value="trusted">trusted</option>
390
+ </select>
391
+ </label>
392
+ <label>
393
+ Source
394
+ <select id="trustZoneSourceClass">
395
+ <option value="">All</option>
396
+ <option value="tool_output">tool_output</option>
397
+ <option value="web_content">web_content</option>
398
+ <option value="subagent_trace">subagent_trace</option>
399
+ <option value="system_memory">system_memory</option>
400
+ <option value="user_input">user_input</option>
401
+ <option value="manual">manual</option>
402
+ </select>
403
+ </label>
404
+ <label>
405
+ Page Size
406
+ <select id="trustZonePageSize">
407
+ <option value="8">8</option>
408
+ <option value="12" selected>12</option>
409
+ <option value="20">20</option>
410
+ </select>
411
+ </label>
412
+ <button id="refreshTrustZonesButton">Refresh</button>
413
+ <button class="secondary" id="previewTrustZoneSeedButton">Preview Demo Seed</button>
414
+ <button class="accent" id="seedTrustZoneDemoButton">Seed Demo Dataset</button>
415
+ </div>
416
+ <div class="toolbar pager" style="margin-top: 12px;">
417
+ <div class="status" id="trustZoneStatus">No trust-zone data loaded yet.</div>
418
+ <div class="toolbar">
419
+ <button class="secondary" id="trustZonePrevButton">Previous</button>
420
+ <div class="status" id="trustZonePageStatus">Page 1</div>
421
+ <button class="secondary" id="trustZoneNextButton">Next</button>
422
+ </div>
423
+ </div>
424
+ <div class="list" id="trustZoneList"></div>
425
+ </section>
426
+
427
+ <section class="card">
428
+ <h2>Trust Zone Detail</h2>
429
+ <div class="status" id="trustZoneDetailStatus">Select a trust-zone record to inspect raw lineage and promotion state.</div>
430
+ <div class="mono-box" style="margin-top: 12px;"><pre id="trustZoneDetail">Trust-zone detail will appear here.</pre></div>
431
+ <div class="mono-box" style="margin-top: 12px;"><pre id="trustZoneSeedResult">Demo seed previews and promotion results will appear here.</pre></div>
432
+ </section>
433
+ </div>
434
+
363
435
  <div class="grid" style="margin-top: 16px;">
364
436
  <section class="card">
365
437
  <h2>Review Queue</h2>
@@ -2227,7 +2227,13 @@ ${sanitized.text}
2227
2227
  invalidateAllMemoriesCache() {
2228
2228
  _StorageManager.allMemoriesInFlight.delete(this.baseDir);
2229
2229
  }
2230
- async _readAllMemoriesFromDisk() {
2230
+ normalizeMemoryReadBatchSize(batchSize) {
2231
+ if (typeof batchSize !== "number" || !Number.isFinite(batchSize)) {
2232
+ return 50;
2233
+ }
2234
+ return Math.max(1, Math.floor(batchSize));
2235
+ }
2236
+ async collectActiveMemoryPaths() {
2231
2237
  const filePaths = [];
2232
2238
  const collectPaths = async (dir) => {
2233
2239
  try {
@@ -2249,11 +2255,14 @@ ${sanitized.text}
2249
2255
  };
2250
2256
  await collectPaths(this.factsDir);
2251
2257
  await collectPaths(this.correctionsDir);
2258
+ return filePaths;
2259
+ }
2260
+ async readParsedMemoriesFromPaths(filePaths, batchSize) {
2252
2261
  if (filePaths.length === 0) return [];
2253
- const BATCH_SIZE = 50;
2262
+ const normalizedBatchSize = this.normalizeMemoryReadBatchSize(batchSize);
2254
2263
  const memories = [];
2255
- for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
2256
- const batch = filePaths.slice(i, i + BATCH_SIZE);
2264
+ for (let i = 0; i < filePaths.length; i += normalizedBatchSize) {
2265
+ const batch = filePaths.slice(i, i + normalizedBatchSize);
2257
2266
  const results = await Promise.all(
2258
2267
  batch.map(async (fullPath) => {
2259
2268
  try {
@@ -2273,12 +2282,126 @@ ${sanitized.text}
2273
2282
  }
2274
2283
  })
2275
2284
  );
2276
- for (const m of results) {
2277
- if (m !== null) memories.push(m);
2285
+ for (const memory of results) {
2286
+ if (memory !== null) memories.push(memory);
2278
2287
  }
2279
2288
  }
2280
2289
  return memories;
2281
2290
  }
2291
+ async readWindowUpdatedMs(filePath) {
2292
+ try {
2293
+ const raw = await readFile2(filePath, "utf-8");
2294
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
2295
+ if (!match) return null;
2296
+ const frontmatterBlock = match[1];
2297
+ const rawUpdated = frontmatterBlock.match(/^updated:\s*"?([^"\n]*)"?/m)?.[1] ?? frontmatterBlock.match(/^created:\s*"?([^"\n]*)"?/m)?.[1] ?? null;
2298
+ const updatedMs = rawUpdated ? Date.parse(rawUpdated) : Number.NaN;
2299
+ return Number.isFinite(updatedMs) ? updatedMs : null;
2300
+ } catch {
2301
+ return null;
2302
+ }
2303
+ }
2304
+ async filterWindowPathsByUpdatedAfter(filePaths, updatedAfterMs) {
2305
+ const results = await Promise.all(filePaths.map(async (filePath) => {
2306
+ const updatedMs = await this.readWindowUpdatedMs(filePath);
2307
+ if (updatedMs !== null) {
2308
+ return updatedMs >= updatedAfterMs ? filePath : null;
2309
+ }
2310
+ try {
2311
+ const fileStat = await stat2(filePath);
2312
+ return fileStat.mtimeMs >= updatedAfterMs ? filePath : null;
2313
+ } catch {
2314
+ return filePath;
2315
+ }
2316
+ }));
2317
+ return results.filter((filePath) => filePath !== null);
2318
+ }
2319
+ orderWindowPaths(filePaths) {
2320
+ const correctionPaths = [];
2321
+ const factPaths = [];
2322
+ for (const filePath of filePaths) {
2323
+ if (filePath === this.correctionsDir || filePath.startsWith(`${this.correctionsDir}${path4.sep}`)) {
2324
+ correctionPaths.push(filePath);
2325
+ } else {
2326
+ factPaths.push(filePath);
2327
+ }
2328
+ }
2329
+ correctionPaths.sort((left, right) => right.localeCompare(left));
2330
+ factPaths.sort((left, right) => right.localeCompare(left));
2331
+ if (correctionPaths.length === 0) return factPaths;
2332
+ if (factPaths.length === 0) return correctionPaths;
2333
+ const ordered = [];
2334
+ const maxLength = Math.max(correctionPaths.length, factPaths.length);
2335
+ for (let i = 0; i < maxLength; i += 1) {
2336
+ const correctionPath = correctionPaths[i];
2337
+ if (correctionPath) ordered.push(correctionPath);
2338
+ const factPath = factPaths[i];
2339
+ if (factPath) ordered.push(factPath);
2340
+ }
2341
+ return ordered;
2342
+ }
2343
+ async readWindowBoundedBatch(candidateBatchPaths, remainingSlots, remainingInspectionBudget, readBatchSize) {
2344
+ const memories = [];
2345
+ const filePaths = [];
2346
+ const normalizedReadBatchSize = this.normalizeMemoryReadBatchSize(readBatchSize);
2347
+ for (let index = 0; index < candidateBatchPaths.length; ) {
2348
+ if (memories.length >= remainingSlots || filePaths.length >= remainingInspectionBudget) break;
2349
+ const availableSlots = remainingSlots - memories.length;
2350
+ const availableInspectionBudget = remainingInspectionBudget - filePaths.length;
2351
+ const parallelWindow = availableSlots >= 4 && availableInspectionBudget >= 4 ? Math.min(normalizedReadBatchSize, 4) : 1;
2352
+ const candidatePaths = candidateBatchPaths.slice(
2353
+ index,
2354
+ index + Math.min(parallelWindow, availableInspectionBudget)
2355
+ );
2356
+ index += candidatePaths.length;
2357
+ if (candidatePaths.length === 0) break;
2358
+ filePaths.push(...candidatePaths);
2359
+ const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length);
2360
+ if (parsedMemories.length === 0) continue;
2361
+ memories.push(...parsedMemories.slice(0, availableSlots));
2362
+ }
2363
+ return { memories, filePaths };
2364
+ }
2365
+ async readMemoriesWindow(options = {}) {
2366
+ const allPaths = await this.collectActiveMemoryPaths();
2367
+ const sortedPaths = this.orderWindowPaths(allPaths);
2368
+ const maxMemories = typeof options.maxMemories === "number" && Number.isFinite(options.maxMemories) ? Math.max(1, Math.floor(options.maxMemories)) : void 0;
2369
+ const maxCandidatePaths = maxMemories === void 0 ? void 0 : maxMemories * 2;
2370
+ const updatedAfterMs = options.updatedAfter?.getTime();
2371
+ const normalizedBatchSize = this.normalizeMemoryReadBatchSize(options.batchSize);
2372
+ const memories = [];
2373
+ const selectedPaths = [];
2374
+ for (let i = 0; i < sortedPaths.length; i += normalizedBatchSize) {
2375
+ if (maxMemories !== void 0 && (memories.length >= maxMemories || maxCandidatePaths !== void 0 && selectedPaths.length >= maxCandidatePaths)) {
2376
+ return { memories, filePaths: selectedPaths };
2377
+ }
2378
+ const batchPaths = sortedPaths.slice(i, i + normalizedBatchSize);
2379
+ const candidateBatchPaths = updatedAfterMs === void 0 ? batchPaths : await this.filterWindowPathsByUpdatedAfter(batchPaths, updatedAfterMs);
2380
+ const remainingSlots = maxMemories === void 0 ? void 0 : Math.max(0, maxMemories - memories.length);
2381
+ const remainingInspectionBudget = maxCandidatePaths === void 0 ? void 0 : Math.max(0, maxCandidatePaths - selectedPaths.length);
2382
+ const { memories: batchMemories, filePaths: parsedCandidatePaths } = remainingSlots === void 0 ? {
2383
+ memories: await this.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize),
2384
+ filePaths: candidateBatchPaths
2385
+ } : await this.readWindowBoundedBatch(
2386
+ candidateBatchPaths,
2387
+ remainingSlots,
2388
+ remainingInspectionBudget ?? remainingSlots,
2389
+ normalizedBatchSize
2390
+ );
2391
+ selectedPaths.push(...parsedCandidatePaths);
2392
+ for (const memory of batchMemories) {
2393
+ memories.push(memory);
2394
+ if (maxMemories !== void 0 && memories.length >= maxMemories) {
2395
+ return { memories, filePaths: selectedPaths };
2396
+ }
2397
+ }
2398
+ }
2399
+ return { memories, filePaths: selectedPaths };
2400
+ }
2401
+ async _readAllMemoriesFromDisk() {
2402
+ const filePaths = await this.collectActiveMemoryPaths();
2403
+ return this.readParsedMemoriesFromPaths(filePaths, 50);
2404
+ }
2282
2405
  /**
2283
2406
  * Read archived memory markdown files under archive/.
2284
2407
  * Used by long-term recall fallback when hot recall has no hits.
@@ -4088,4 +4211,4 @@ export {
4088
4211
  serializeEntityFile,
4089
4212
  StorageManager
4090
4213
  };
4091
- //# sourceMappingURL=chunk-Y6DMH5LN.js.map
4214
+ //# sourceMappingURL=chunk-IADQJS2J.js.map