@modusensus/dsh-mneme 0.7.10 → 0.7.12
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.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -18,8 +18,6 @@ import { randomUUID, createHash } from "node:crypto";
|
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
20
|
import { buildReceipt } from "../dream.js";
|
|
21
|
-
import { computeHeat } from "../heat.js";
|
|
22
|
-
import { extractEntities } from "../entities/extractor.js";
|
|
23
21
|
|
|
24
22
|
const SUMMARY_MAX = 120;
|
|
25
23
|
// Conflict similarity threshold per strictness level (v0.4.0):
|
|
@@ -74,23 +72,16 @@ async function streamText(ctx, options) {
|
|
|
74
72
|
return text;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// can pin a cheaper model for its bulk passes without disturbing the dream
|
|
81
|
-
// route. The agent-default-last ordering matters: in a standard DSH install
|
|
82
|
-
// agentDefaultModel always resolves, so putting it first would make every
|
|
83
|
-
// config route dead code.
|
|
75
|
+
/** LLM route: agent default model first, then sleepProvider/Model, then the
|
|
76
|
+
* dream route as a shared fallback. Sleep can pin a cheaper model for its
|
|
77
|
+
* bulk passes without disturbing the dream route. */
|
|
84
78
|
function resolveSleepRoute(ctx, config, logger) {
|
|
85
|
-
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
86
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
87
79
|
try {
|
|
88
80
|
const sel = ctx?.agentDefaultModel?.currentSelection?.();
|
|
89
|
-
if (sel?.provider && sel?.model) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch { /* fall through */ }
|
|
81
|
+
if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
|
|
82
|
+
} catch { /* fall through to config route */ }
|
|
83
|
+
if (config.sleepProvider && config.sleepModel) return { provider: config.sleepProvider, model: config.sleepModel };
|
|
84
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
94
85
|
logger?.warn?.("dsh-mneme sleep: no llm route available");
|
|
95
86
|
return undefined;
|
|
96
87
|
}
|
|
@@ -117,7 +108,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
117
108
|
}
|
|
118
109
|
const strictness = config.sleepConflictStrictness ?? "normal";
|
|
119
110
|
const threshold = CONFLICT_THRESHOLDS[strictness] ?? CONFLICT_THRESHOLDS.normal;
|
|
120
|
-
const memories = service.all().filter((m) => !m.archived && !m.
|
|
111
|
+
const memories = service.all().filter((m) => !m.archived && !m.forgotten && m.type !== "summary");
|
|
121
112
|
if (memories.length < 2) return { status: "skipped", reason: "too few memories" };
|
|
122
113
|
if (signal?.aborted) return { status: "aborted", reason: "user activity" };
|
|
123
114
|
|
|
@@ -248,22 +239,11 @@ function phaseDemotion(service, config, logger, runId, signal = null) {
|
|
|
248
239
|
const archived = [];
|
|
249
240
|
for (const m of service.all()) {
|
|
250
241
|
if (signal?.aborted) break;
|
|
251
|
-
if (m.archived || m.forgotten
|
|
252
|
-
|
|
253
|
-
// fallback updated_at —— 合并/更新刷的 updated_at 不是访问,防止
|
|
254
|
-
// autoDream 合并动作重置新鲜度时钟而伪装成"刚被召回"。
|
|
255
|
-
const ref = m.last_accessed_at ?? m.created_at;
|
|
242
|
+
if (m.archived || m.forgotten) continue;
|
|
243
|
+
const ref = m.last_accessed_at ?? m.updated_at ?? m.created_at;
|
|
256
244
|
if (!ref) continue;
|
|
257
245
|
const t = new Date(ref).getTime();
|
|
258
246
|
if (Number.isNaN(t)) continue;
|
|
259
|
-
// v0.7.0 热联合判定(待办③):时间窗之外再加两道保护闸——热度低于
|
|
260
|
-
// sleepHeatThreshold 且 importance<5 才允许降级。λ=0 的免疫类型 heat 恒
|
|
261
|
-
// 1.0 天然豁免(preference/pattern/summary 永不因 sleep 降级);importance
|
|
262
|
-
// ≥5 的紧要记忆无论多冷都保留。`冷但重要` 与 `热但低值` 均不满足条件。
|
|
263
|
-
const heat = computeHeat(m, Date.now(), config);
|
|
264
|
-
const heatProtected = heat >= (config.sleepHeatThreshold ?? 0.05);
|
|
265
|
-
const important = (m.importance ?? 0) >= 5;
|
|
266
|
-
if (heatProtected || important) continue;
|
|
267
247
|
if (t < compressCut) {
|
|
268
248
|
service.setArchived(m.id, true);
|
|
269
249
|
archived.push(m.id);
|
|
@@ -293,7 +273,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
293
273
|
const limit = config.sleepPatternMinMemories ?? 100;
|
|
294
274
|
const memories = service
|
|
295
275
|
.list({ limit: 200, includeForgotten: false })
|
|
296
|
-
.filter((m) => !m.archived &&
|
|
276
|
+
.filter((m) => !m.archived && m.type !== "summary" && m.type !== "pattern")
|
|
297
277
|
.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1))
|
|
298
278
|
.slice(0, limit);
|
|
299
279
|
if (memories.length === 0) return { status: "skipped", reason: "no memories to scan" };
|
|
@@ -362,7 +342,7 @@ function phaseRelations(service, config, logger, runId, signal = null) {
|
|
|
362
342
|
if (entities.length < 2) return { status: "skipped", reason: "too few entities" };
|
|
363
343
|
const orphans = entities.filter((e) => (service.getRelations(e.id) ?? []).length === 0);
|
|
364
344
|
if (orphans.length === 0) return { status: "skipped", reason: "no orphan entities" };
|
|
365
|
-
const memories = service.all().filter((m) => !m.archived && !m.
|
|
345
|
+
const memories = service.all().filter((m) => !m.archived && !m.forgotten);
|
|
366
346
|
const seen = new Set();
|
|
367
347
|
const related = [];
|
|
368
348
|
const MAX_RELATIONS_PER_ORPHAN = 3;
|
|
@@ -397,123 +377,6 @@ function phaseRelations(service, config, logger, runId, signal = null) {
|
|
|
397
377
|
};
|
|
398
378
|
}
|
|
399
379
|
|
|
400
|
-
/**
|
|
401
|
-
* Phase 4.5 — batch entity extraction. The write-path extractor (index.js)
|
|
402
|
-
* only fires when entityExtractionEnabled is on — one LLM call per write, a
|
|
403
|
-
* deliberate cost decision — so default installs never accumulate entities,
|
|
404
|
-
* which is exactly why the ego-graph panel (issue #23) renders blank for users
|
|
405
|
-
* with plenty of memories. This phase backfills the entity graph in bulk:
|
|
406
|
-
* memories that carry no entity attrs yet are passed through extractEntities
|
|
407
|
-
* one at a time (oldest first, capped per run). Write-path stays untouched —
|
|
408
|
-
* this is an additive channel for sleep users. Fail-safe per memory: one bad
|
|
409
|
-
* extract never aborts the phase or the cycle.
|
|
410
|
-
*/
|
|
411
|
-
async function phaseEntityExtraction(ctx, service, config, logger, runId, signal = null) {
|
|
412
|
-
if (config.sleepEntityExtractionEnabled !== true) {
|
|
413
|
-
return { status: "skipped", reason: "disabled" };
|
|
414
|
-
}
|
|
415
|
-
const route = resolveSleepRoute(ctx, config, logger);
|
|
416
|
-
if (!route) return { status: "skipped", reason: "no llm route" };
|
|
417
|
-
const maxPerRun = config.sleepEntityExtractionMaxPerRun ?? 20;
|
|
418
|
-
|
|
419
|
-
// Oldest un-extracted first. The store returns bounded pages via SQL (no
|
|
420
|
-
// O(N) full-table scan); JS pages through offsets and only pays the
|
|
421
|
-
// getAttrsByMemory cross-table check on each page, stopping once it has
|
|
422
|
-
// maxPerRun un-extracted memories or the store is exhausted. Every memory
|
|
423
|
-
// this phase touches gets stamped entity_extracted_at (successful or
|
|
424
|
-
// entity-less extracts alike) so a text with no extractable entities is not
|
|
425
|
-
// re-queued forever; failures are NOT stamped, so a transient LLM hiccup
|
|
426
|
-
// retries next cycle.
|
|
427
|
-
const candidates = [];
|
|
428
|
-
for (let offset = 0; candidates.length < maxPerRun; offset += 100) {
|
|
429
|
-
const page = service.listForEntityExtraction({ limit: 100, offset });
|
|
430
|
-
if (page.length === 0) break;
|
|
431
|
-
candidates.push(...page.filter((m) => (service.getAttrsByMemory?.(m.id) ?? []).length === 0));
|
|
432
|
-
}
|
|
433
|
-
candidates.length = Math.min(candidates.length, maxPerRun);
|
|
434
|
-
if (candidates.length === 0) return { status: "skipped", reason: "no memories to extract" };
|
|
435
|
-
if (signal?.aborted) return { status: "aborted", reason: "user activity" };
|
|
436
|
-
|
|
437
|
-
// extractEntities expects callLLM(messages, options) → text; adapt sleep's
|
|
438
|
-
// streamText + route resolution (same precedence as the other phases).
|
|
439
|
-
const callLLM = async (messages) => {
|
|
440
|
-
const r = resolveSleepRoute(ctx, config, logger);
|
|
441
|
-
if (!r) return undefined;
|
|
442
|
-
return streamText(ctx, {
|
|
443
|
-
...r,
|
|
444
|
-
purpose: "sleep-entity-extract",
|
|
445
|
-
maxTokens: 4096,
|
|
446
|
-
...(config.sleepReasoningEffort && config.sleepReasoningEffort !== "none"
|
|
447
|
-
? { reasoningEffort: config.sleepReasoningEffort }
|
|
448
|
-
: {}),
|
|
449
|
-
messages
|
|
450
|
-
});
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
let extracted = 0;
|
|
454
|
-
let failed = 0;
|
|
455
|
-
let aborted = false;
|
|
456
|
-
const extractedIds = [];
|
|
457
|
-
for (const m of candidates) {
|
|
458
|
-
if (signal?.aborted) {
|
|
459
|
-
aborted = true;
|
|
460
|
-
break;
|
|
461
|
-
}
|
|
462
|
-
const now = new Date().toISOString();
|
|
463
|
-
try {
|
|
464
|
-
// Stamp a pending marker BEFORE the LLM call so a crash mid-extract (the
|
|
465
|
-
// stamp and the entity writes are not one transaction) cannot re-queue
|
|
466
|
-
// this memory while we are already working it. Cleared on outcome.
|
|
467
|
-
service.setMemoryMetadata?.(m.id, { pending_extracted_at: now });
|
|
468
|
-
} catch (error) {
|
|
469
|
-
logger?.warn?.(`dsh-mneme sleep: failed to stamp pending_extracted_at for ${m.id}: ${String(error)}`);
|
|
470
|
-
failed++;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
try {
|
|
474
|
-
const result = await extractEntities(m, { store: service, config, callLLM, logger });
|
|
475
|
-
if (result?.ok) {
|
|
476
|
-
extracted++;
|
|
477
|
-
extractedIds.push(m.id);
|
|
478
|
-
// Stamp done regardless of whether the LLM found entities — an empty
|
|
479
|
-
// extract is still a definitive answer for this memory.
|
|
480
|
-
try {
|
|
481
|
-
service.setMemoryMetadata?.(m.id, { entity_extracted_at: now, pending_extracted_at: null });
|
|
482
|
-
} catch (error) {
|
|
483
|
-
logger?.warn?.(`dsh-mneme sleep: failed to stamp entity_extracted_at for ${m.id}: ${String(error)}`);
|
|
484
|
-
}
|
|
485
|
-
} else {
|
|
486
|
-
failed++;
|
|
487
|
-
logger?.warn?.(`dsh-mneme sleep: entity extraction failed for ${m.id}: ${result?.error ?? "unknown"}`);
|
|
488
|
-
try {
|
|
489
|
-
service.setMemoryMetadata?.(m.id, { pending_extracted_at: null });
|
|
490
|
-
} catch (error) {
|
|
491
|
-
logger?.warn?.(`dsh-mneme sleep: failed to clear pending_extracted_at for ${m.id}: ${String(error)}`);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
} catch (error) {
|
|
495
|
-
failed++;
|
|
496
|
-
logger?.warn?.(`dsh-mneme sleep: entity extraction threw for ${m.id}: ${String(error)}`);
|
|
497
|
-
try {
|
|
498
|
-
service.setMemoryMetadata?.(m.id, { pending_extracted_at: null });
|
|
499
|
-
} catch (err) {
|
|
500
|
-
logger?.warn?.(`dsh-mneme sleep: failed to clear pending_extracted_at for ${m.id}: ${String(err)}`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return {
|
|
505
|
-
// Any success counts as ok (failures are surfaced via detail.failed + warn,
|
|
506
|
-
// matching phaseConflicts); all-failed reports failed so deriveStatus can
|
|
507
|
-
// reflect a broken route without aborting the other phases.
|
|
508
|
-
status: aborted ? "aborted" : extracted > 0 ? "ok" : failed > 0 ? "failed" : "noop",
|
|
509
|
-
scanned: candidates.length,
|
|
510
|
-
extracted,
|
|
511
|
-
failed,
|
|
512
|
-
aborted,
|
|
513
|
-
extractedIds
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
380
|
// ---------------------------------------------------------------- run
|
|
518
381
|
|
|
519
382
|
function deriveStatus(phases) {
|
|
@@ -547,9 +410,6 @@ export async function runSleep(ctx, service, config, logger, semantic = null, si
|
|
|
547
410
|
await attempt("conflicts", () => phaseConflicts(ctx, service, config, logger, runId, semantic, signal));
|
|
548
411
|
await attempt("demotion", () => phaseDemotion(service, config, logger, runId, signal));
|
|
549
412
|
await attempt("patterns", () => phasePatterns(ctx, service, config, logger, runId, signal));
|
|
550
|
-
// entity-extraction runs before relation completion so freshly minted
|
|
551
|
-
// entities get their orphan relations completed in the same cycle.
|
|
552
|
-
await attempt("entity-extraction", () => phaseEntityExtraction(ctx, service, config, logger, runId, signal));
|
|
553
413
|
await attempt("relations", () => phaseRelations(service, config, logger, runId, signal));
|
|
554
414
|
|
|
555
415
|
const status = deriveStatus(phases);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
|
-
import { runAutoTag } from "./dream/tag-extractor.js";
|
|
4
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
export { validateDecisions, applyDecisions
|
|
4
|
+
export { validateDecisions, applyDecisions };
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -41,95 +40,6 @@ function extractJsonArray(text) {
|
|
|
41
40
|
}
|
|
42
41
|
return null;
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Field-name drift guard (v0.5.3): thinking-type models (deepseek-v4-flash
|
|
47
|
-
* etc.) occasionally ignore the prompt's exact decision schema and emit alias
|
|
48
|
-
* keys —实测方案 A 输出 "consolidation"/"target_ids"、方案 B 输出
|
|
49
|
-
* "action"/"targetIds",都不是插件要求的 "action"/"ids"。normalizeDecisions
|
|
50
|
-
* 在 validateDecisions 之前把常见变体重写回规范字段名,让"字段名不听话但
|
|
51
|
-
* 语义正确"的输出仍可被应用,而不是整单被拒。覆盖三类漂移:
|
|
52
|
-
* 1. 整个 body 是包在对象里的数组({consolidation:[...]} /
|
|
53
|
-
* {decisions:[...]} / {actions:[...]});
|
|
54
|
-
* 2. 决策对象用了别名键(target_ids→ids、keep_source→keepSource、
|
|
55
|
-
* winner_id→winner、targetIds→ids 等);
|
|
56
|
-
* 3. action 值用了同义词(archived→archive、consolidation→merge 等)。
|
|
57
|
-
* 无归一化必要时原样返回(null→null,调用方"no json array"分支不受影响)。
|
|
58
|
-
*/
|
|
59
|
-
const FIELD_ALIASES = {
|
|
60
|
-
// "consolidation" 也可作 action 键(实测 deepseek-v4-flash 这么写过);
|
|
61
|
-
// 但 normalizeOne 对 action 键做字符串过滤,{consolidation:[...]} 这种
|
|
62
|
-
// wrapper 数组不会被误当成 action(顶层 WRAPPER_KEYS 负责解包)。
|
|
63
|
-
action: ["action", "decision", "operation", "op", "action_type", "actionType", "mode", "consolidation"],
|
|
64
|
-
// 注意:action 别名不含 "type"——create 决策的 type 是合法的记忆类型字段,
|
|
65
|
-
// 不能误当 action。
|
|
66
|
-
ids: ["ids", "target_ids", "targetIds", "targets", "memory_ids", "memoryIds", "id_list", "idList", "memories"],
|
|
67
|
-
reason: ["reason", "rationale", "why", "comment", "note", "explanation"],
|
|
68
|
-
importance: ["importance", "priority", "weight", "level", "score"],
|
|
69
|
-
keepSource: ["keepSource", "keep_source", "source_id", "sourceId", "keeper", "keep_id", "keepId"],
|
|
70
|
-
winner: ["winner", "winner_id", "winnerId", "win_id", "winId", "preferred", "primary"],
|
|
71
|
-
loser: ["loser", "loser_id", "loserId", "lose_id", "loseId", "drop_id", "dropId", "archive_id", "archiveId"],
|
|
72
|
-
title: ["title", "new_title", "newTitle", "merged_title", "mergedTitle", "merge_title", "mergeTitle"],
|
|
73
|
-
content: ["content", "new_content", "newContent", "merged_content", "mergedContent", "merge_content", "mergeContent"],
|
|
74
|
-
evidence: ["evidence", "evidence_ids", "evidenceIds"]
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// 保守的 action 值同义词:只收语义无歧义、不可能被误认为记忆类型/其他 action 的映射。
|
|
78
|
-
const ACTION_SYNONYMS = {
|
|
79
|
-
archived: "archive",
|
|
80
|
-
remove: "archive",
|
|
81
|
-
delete: "archive",
|
|
82
|
-
combine: "merge",
|
|
83
|
-
consolidation: "merge",
|
|
84
|
-
consolidate: "merge",
|
|
85
|
-
modify: "update",
|
|
86
|
-
edit: "update"
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const WRAPPER_KEYS = ["consolidation", "decisions", "actions", "results", "updates", "list", "data"];
|
|
90
|
-
|
|
91
|
-
/** 把单个决策对象重写到规范字段名;非对象原样返回。 */
|
|
92
|
-
function normalizeOne(raw) {
|
|
93
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
94
|
-
const d = {};
|
|
95
|
-
const consumed = new Set();
|
|
96
|
-
for (const [canon, aliases] of Object.entries(FIELD_ALIASES)) {
|
|
97
|
-
for (const key of aliases) {
|
|
98
|
-
if (key in raw && raw[key] !== undefined && raw[key] !== null) {
|
|
99
|
-
// action 必须是字符串——{consolidation:[...]} 的 wrapper 数组跳过。
|
|
100
|
-
if (canon === "action" && Array.isArray(raw[key])) continue;
|
|
101
|
-
d[canon] = raw[key];
|
|
102
|
-
consumed.add(key);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// 保留未被别名消费的原始键(create 的 type、未来字段)原样透传。
|
|
108
|
-
for (const key of Object.keys(raw)) {
|
|
109
|
-
if (!consumed.has(key)) d[key] = raw[key];
|
|
110
|
-
}
|
|
111
|
-
if (typeof d.action === "string") {
|
|
112
|
-
const low = d.action.toLowerCase();
|
|
113
|
-
if (ACTION_SYNONYMS[low] !== undefined) d.action = ACTION_SYNONYMS[low];
|
|
114
|
-
}
|
|
115
|
-
// ids 若被写成单个字符串则包成数组(validateDecisions 要求数组)。
|
|
116
|
-
if (d.ids !== undefined && typeof d.ids === "string") d.ids = [d.ids];
|
|
117
|
-
return d;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** 归一化 LLM 决策 body(数组 / 包裹对象 / 单个决策对象),null 原样返回。 */
|
|
121
|
-
function normalizeDecisions(raw) {
|
|
122
|
-
if (!raw) return raw;
|
|
123
|
-
if (Array.isArray(raw)) return raw.map(normalizeOne).filter((d) => d && typeof d === "object");
|
|
124
|
-
if (typeof raw === "object") {
|
|
125
|
-
for (const key of WRAPPER_KEYS) {
|
|
126
|
-
if (Array.isArray(raw[key])) return normalizeDecisions(raw[key]);
|
|
127
|
-
}
|
|
128
|
-
// 单个决策对象 → 包成单元素数组(validateDecisions 要求数组)。
|
|
129
|
-
return [normalizeOne(raw)];
|
|
130
|
-
}
|
|
131
|
-
return raw;
|
|
132
|
-
}
|
|
133
43
|
const SUMMARY_PROMPT = `你是记忆库摘要助手。根据整理后的记忆,生成一段 150-200 字的记忆库总览,覆盖:用户偏好、活跃项目、关键决策。之后作为会话上下文注入。只输出摘要文本,不要其他内容。`;
|
|
134
44
|
|
|
135
45
|
const CONSOLIDATION_PROMPT = `你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。
|
|
@@ -401,26 +311,20 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
401
311
|
}
|
|
402
312
|
|
|
403
313
|
/**
|
|
404
|
-
* Resolve the LLM route (
|
|
405
|
-
* (dreamProvider/dreamModel)
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* this ordering the config route would be dead code and dreamProvider/dreamModel
|
|
409
|
-
* could never take effect. Falls through to undefined when no route exists —
|
|
410
|
-
* runDream then fails safe. A config→default switch is logged so it is observable.
|
|
314
|
+
* Resolve the LLM route: agent default model (deployment) first, plugin config
|
|
315
|
+
* (dreamProvider/dreamModel) as fallback. Falls through to undefined when no
|
|
316
|
+
* route exists — runDream then fails safe. Fallback is logged so a silent
|
|
317
|
+
* route switch is observable.
|
|
411
318
|
*/
|
|
412
319
|
function resolveRoute(ctx, config, logger) {
|
|
413
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
414
320
|
try {
|
|
415
321
|
const sel = ctx.agentDefaultModel?.currentSelection?.();
|
|
416
|
-
if (sel?.provider && sel?.model) {
|
|
417
|
-
|
|
418
|
-
return { provider: sel.provider, model: sel.model };
|
|
419
|
-
}
|
|
420
|
-
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
|
|
322
|
+
if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
|
|
323
|
+
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, falling back to config route");
|
|
421
324
|
} catch (error) {
|
|
422
|
-
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
|
|
325
|
+
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed, falling back to config route: ${String(error)}`);
|
|
423
326
|
}
|
|
327
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
424
328
|
return undefined;
|
|
425
329
|
}
|
|
426
330
|
|
|
@@ -502,7 +406,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
502
406
|
let inFlight = null;
|
|
503
407
|
|
|
504
408
|
function shouldTrigger(service) {
|
|
505
|
-
const memories = service.all().filter((m) => !m.archived &&
|
|
409
|
+
const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
506
410
|
const count = memories.length;
|
|
507
411
|
const chars = totalChars(memories);
|
|
508
412
|
const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
|
|
@@ -559,7 +463,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
559
463
|
if (inFlight) await inFlight.catch(() => {});
|
|
560
464
|
}
|
|
561
465
|
|
|
562
|
-
async function runDream(ctx, service, config
|
|
466
|
+
async function runDream(ctx, service, config) {
|
|
563
467
|
const logger = ctx.logger;
|
|
564
468
|
let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
565
469
|
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
@@ -712,35 +616,23 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
712
616
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
713
617
|
}
|
|
714
618
|
|
|
715
|
-
|
|
716
|
-
// 在 validateDecisions 之前重写到规范字段名,语义正确但 schema 不听话的
|
|
717
|
-
// 输出不再整单被拒。
|
|
718
|
-
const decisions = normalizeDecisions(extractJsonArray(decisionText));
|
|
619
|
+
const decisions = extractJsonArray(decisionText);
|
|
719
620
|
if (!Array.isArray(decisions)) {
|
|
720
621
|
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0})`);
|
|
721
622
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
722
623
|
}
|
|
723
|
-
const { ok, errors
|
|
624
|
+
const { ok, errors } = validateDecisions(decisions, snapshot, {
|
|
724
625
|
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
725
626
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
726
627
|
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
727
628
|
// 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
|
|
728
629
|
dreamImplicitKeep: config.dreamImplicitKeep,
|
|
729
|
-
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
|
|
730
|
-
// Issue #26 (P0):跨类型 merge 等"单条非法"决策不再拖垮整批——默认开启
|
|
731
|
-
// skipInvalid,非法决策跳过、合法子集照常应用(run 记为 degraded)。
|
|
732
|
-
// dreamSkipInvalid:false 可恢复旧的"任意非法即整单拒绝"。
|
|
733
|
-
skipInvalid: config.dreamSkipInvalid !== false,
|
|
734
|
-
// Issue #26 (P1):显式开启 allowCrossTypeMerge 后放宽跨类型合并检查。
|
|
735
|
-
allowCrossTypeMerge: config.allowCrossTypeMerge === true
|
|
630
|
+
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
|
|
736
631
|
});
|
|
737
632
|
if (!ok) {
|
|
738
633
|
logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
|
|
739
634
|
return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false });
|
|
740
635
|
}
|
|
741
|
-
for (const s of skipped) {
|
|
742
|
-
logger?.warn?.(`dsh-mneme dream: skipping invalid decision[${s.index}] (${s.action}): ${s.error}`);
|
|
743
|
-
}
|
|
744
636
|
|
|
745
637
|
// Capture pre-update snapshots so the audit records what each update changed.
|
|
746
638
|
const updateSnapshots = {};
|
|
@@ -813,11 +705,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
813
705
|
);
|
|
814
706
|
// Outcome is derived from the ACTUALLY committed sub-steps, never from the
|
|
815
707
|
// raw LLM decision list — a merge whose archive step rolled back must not
|
|
816
|
-
// claim "merge-archived" (item ②). Conflicts/failures
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
// e.g. cross-type merge).
|
|
820
|
-
const outcome = { ...buildOutcome(committed), conflicts, failures, skipped };
|
|
708
|
+
// claim "merge-archived" (item ②). Conflicts/failures ride along so the
|
|
709
|
+
// audit row records why the run diverged.
|
|
710
|
+
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
821
711
|
// Frozen conflicts were not adjudicated: mark both sides pending in the
|
|
822
712
|
// per-id outcome so the audit row shows they were parked, not skipped.
|
|
823
713
|
if (frozenIds.length) {
|
|
@@ -833,30 +723,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
833
723
|
// that only froze conflicts is not a noop.
|
|
834
724
|
const noChange = frozenCount === 0 && applied === 0 && committed.every((c) => c.action === "keep");
|
|
835
725
|
|
|
836
|
-
// v0.6.2 auto-tag: a light LLM pass over the retained (post-consolidation)
|
|
837
|
-
// memories. Opt-in via config.autoTagEnabled, bounded by autoTagMaxPerRun,
|
|
838
|
-
// and always fail-safe — a tag failure must never change the consolidation
|
|
839
|
-
// outcome reported below (it is logged and counted, nothing more).
|
|
840
|
-
let autoTagged = 0;
|
|
841
|
-
// Effective opt-in (issue #31): the Web panel's settings toggle
|
|
842
|
-
// (user_settings) overrides the zod plugin config when explicitly stored;
|
|
843
|
-
// unset → fall back to config.autoTagEnabled. settings is null-safe (absent
|
|
844
|
-
// in tests / direct calls) so behaviour is unchanged without it.
|
|
845
|
-
const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
|
|
846
|
-
if (autoTagEnabled === true) {
|
|
847
|
-
try {
|
|
848
|
-
const tagResult = await runAutoTag({ ctx, service, config, route });
|
|
849
|
-
autoTagged = tagResult?.tagged ?? 0;
|
|
850
|
-
if (tagResult?.ok === false && tagResult?.skippedBy && tagResult.skippedBy !== "empty") {
|
|
851
|
-
logger?.warn?.(`dsh-mneme dream: auto-tag skipped (${tagResult.skippedBy})`);
|
|
852
|
-
} else if (autoTagged > 0) {
|
|
853
|
-
logger?.info?.(`[dsh-mneme] auto-tag: ${autoTagged} memory(ies) tagged`);
|
|
854
|
-
}
|
|
855
|
-
} catch (error) {
|
|
856
|
-
logger?.warn?.(`dsh-mneme dream: auto-tag failed: ${String(error)}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
726
|
// Keep the vector index consistent with the post-dream store state.
|
|
861
727
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
862
728
|
try {
|
|
@@ -886,7 +752,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
886
752
|
: {}),
|
|
887
753
|
messages: [
|
|
888
754
|
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
889
|
-
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived &&
|
|
755
|
+
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
890
756
|
]
|
|
891
757
|
}, reportUsage));
|
|
892
758
|
} catch (error) {
|
|
@@ -919,12 +785,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
919
785
|
// run. ok:false keeps the scheduler from moving the baseline.
|
|
920
786
|
// ok — either real changes landed, or a fresh summary was stored
|
|
921
787
|
// (all-keep + summary is a substantive summary refresh).
|
|
922
|
-
// degraded — real consolidation landed but the
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
// skipped by skipInvalid. The valid subset was absorbed (ok
|
|
926
|
-
// for the baseline — a future run won't re-fail on the same
|
|
927
|
-
// permanently-invalid pairs), but the run is marked, not faked.
|
|
788
|
+
// degraded — real consolidation landed but the summary came back empty/
|
|
789
|
+
// missing: the store was absorbed (ok for the baseline) but
|
|
790
|
+
// the run did not produce its full output (marked, not faked).
|
|
928
791
|
let status;
|
|
929
792
|
let okResult;
|
|
930
793
|
if (partial) {
|
|
@@ -933,13 +796,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
933
796
|
} else if (noChange) {
|
|
934
797
|
status = summaryStored ? "ok" : "noop";
|
|
935
798
|
okResult = summaryStored;
|
|
936
|
-
} else if (skipped.length > 0) {
|
|
937
|
-
// Issue #26: valid subset landed but at least one decision was dropped as
|
|
938
|
-
// invalid (e.g. cross-type merge). degraded (not ok) — never a fake ok.
|
|
939
|
-
// autoTag still runs on degraded runs, so it is not blocked behind a
|
|
940
|
-
// "success" that a skipped-invalid run can never reach.
|
|
941
|
-
status = "degraded";
|
|
942
|
-
okResult = true;
|
|
943
799
|
} else {
|
|
944
800
|
status = summaryStored ? "ok" : "degraded";
|
|
945
801
|
okResult = true;
|
|
@@ -952,10 +808,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
952
808
|
outcome,
|
|
953
809
|
conflicts,
|
|
954
810
|
failures,
|
|
955
|
-
skipped,
|
|
956
811
|
frozen: frozenCount,
|
|
957
|
-
summary: summaryStored
|
|
958
|
-
error: skipped.length > 0 ? `skipped ${skipped.length} invalid decision(s)` : undefined
|
|
812
|
+
summary: summaryStored
|
|
959
813
|
});
|
|
960
814
|
}
|
|
961
815
|
|
|
@@ -21,13 +21,6 @@ export function estimateTokens(text) {
|
|
|
21
21
|
* rounds(): Array, clear(): void}}
|
|
22
22
|
*/
|
|
23
23
|
export function createHotMemory({ maxRounds = 5, maxTokens = 2000 } = {}) {
|
|
24
|
-
// Entry defense: a non-positive or non-integer maxRounds (0, -1, 1.5, NaN,
|
|
25
|
-
// null, "2") would make the eviction while-loop unbounded — the buffer can
|
|
26
|
-
// never shrink below `buffer.length > maxRounds`, so `add` would spin forever.
|
|
27
|
-
// Fall back to the defaults so a hostile/buggy caller can never wedge the
|
|
28
|
-
// hot-memory buffer in an infinite loop.
|
|
29
|
-
maxRounds = (Number.isInteger(maxRounds) && maxRounds > 0) ? maxRounds : 5;
|
|
30
|
-
maxTokens = (Number.isFinite(maxTokens) && maxTokens > 0) ? maxTokens : 2000;
|
|
31
24
|
const buffer = [];
|
|
32
25
|
|
|
33
26
|
function totalTokens() {
|