agent-working-memory 0.15.8 → 0.16.0
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/AGENTS.md +36 -0
- package/README.md +24 -12
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +33 -20
- package/dist/adapters/common.js.map +1 -1
- package/dist/core/embeddings.d.ts +1 -1
- package/dist/core/embeddings.js +1 -1
- package/dist/core/entity-extract.js +1 -1
- package/dist/core/query-expander.d.ts +4 -2
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +4 -2
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/recall-config.d.ts +1 -1
- package/dist/core/recall-config.d.ts.map +1 -1
- package/dist/core/recall-config.js +6 -1
- package/dist/core/recall-config.js.map +1 -1
- package/dist/core/recall-telemetry.d.ts +15 -0
- package/dist/core/recall-telemetry.d.ts.map +1 -0
- package/dist/core/recall-telemetry.js +108 -0
- package/dist/core/recall-telemetry.js.map +1 -0
- package/dist/core/rerank-window.d.ts +5 -4
- package/dist/core/rerank-window.d.ts.map +1 -1
- package/dist/core/rerank-window.js +5 -4
- package/dist/core/rerank-window.js.map +1 -1
- package/dist/core/reranker.d.ts +2 -1
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +23 -3
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.js +1 -1
- package/dist/core/salience.js.map +1 -1
- package/dist/core/token-budget.d.ts +2 -2
- package/dist/core/token-budget.js +2 -2
- package/dist/core/whoami.d.ts +1 -1
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +19 -0
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/engine/activation.d.ts +30 -13
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +159 -50
- package/dist/engine/activation.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +98 -98
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite.d.ts +28 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +55 -0
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +28 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +55 -0
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +28 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +56 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/llms.txt +437 -0
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -1
- package/plugin/bin/awm-mcp-launcher.cjs +2 -2
- package/plugin/skills/awm-memory/SKILL.md +33 -20
- package/src/adapters/common.ts +33 -20
- package/src/core/embeddings.ts +1 -1
- package/src/core/entity-extract.ts +1 -1
- package/src/core/query-expander.ts +4 -2
- package/src/core/recall-config.ts +6 -1
- package/src/core/recall-telemetry.ts +131 -0
- package/src/core/rerank-window.ts +159 -158
- package/src/core/reranker.ts +24 -3
- package/src/core/salience.ts +1 -1
- package/src/core/token-budget.ts +160 -160
- package/src/core/whoami.ts +120 -120
- package/src/core/write-pipeline.ts +36 -18
- package/src/engine/activation.ts +159 -51
- package/src/index.ts +1 -1
- package/src/mcp.ts +1475 -1475
- package/src/storage/pglite.ts +49 -0
- package/src/storage/postgres.ts +49 -0
- package/src/storage/sqlite.ts +51 -0
package/src/adapters/common.ts
CHANGED
|
@@ -71,7 +71,7 @@ export function resolveHookSecret(dbPath: string): string {
|
|
|
71
71
|
* Retrieval settings `awm setup` turns on for a new install (0.14.6).
|
|
72
72
|
*
|
|
73
73
|
* All three ship default-off in the engine so an existing process never changes
|
|
74
|
-
*
|
|
74
|
+
* behavior on upgrade, but the README has recommended them together since 0.14.0:
|
|
75
75
|
* second-stage rerank by the cross-encoder's own score, a 400-char rerank window
|
|
76
76
|
* on the densest query-term region (25% → 87.5% on long memories), and tags in the
|
|
77
77
|
* rerank passage (+7.4pp). A value the user already has in their config wins.
|
|
@@ -381,9 +381,11 @@ you write them; AWM stays current because every agent reads + writes the same st
|
|
|
381
381
|
than one filesystem search.
|
|
382
382
|
4. **As you learn things**: call \`memory_write\` proactively. Don't batch.
|
|
383
383
|
5. **Finishing a task**: call \`memory_task_end\` with a summary.
|
|
384
|
-
6. **Hooks do the rest.** \`awm setup\` installs
|
|
385
|
-
each prompt and injects what clears confidence (or nothing),
|
|
386
|
-
compaction and session end (plus a 15-min timer)
|
|
384
|
+
6. **Hooks do the rest.** \`awm setup\` installs five: a **prime** hook that recalls against
|
|
385
|
+
each prompt and injects what clears confidence (or nothing), checkpoint hooks on
|
|
386
|
+
compaction and session end (plus a 15-min timer), a local write/recall reminder on
|
|
387
|
+
Stop, and a reminder after Bash/PowerShell calls that a data change is not done
|
|
388
|
+
until it is written. Primed context arrives labeled
|
|
387
389
|
\`[class · age]\` — treat it by the volatility rubric, not as user input. If a turn
|
|
388
390
|
arrives with no primed context, that is a signal too: recall explicitly before
|
|
389
391
|
asserting anything.
|
|
@@ -411,9 +413,9 @@ extraction all see most strongly.
|
|
|
411
413
|
a noisy bucket where it competes with everything else in the area.
|
|
412
414
|
- **Include 2+ retrievable identifiers.** File paths, function names, table
|
|
413
415
|
columns, ticket IDs, exact error strings, the literal terms a future query
|
|
414
|
-
will use. \`
|
|
416
|
+
will use. \`SettlementService.closeRun()\` beats "the accounting code."
|
|
415
417
|
\`tblMemberDetails.activation_date\` beats "the activation column."
|
|
416
|
-
\`schema/072-
|
|
418
|
+
\`schema/072-settlement-lock.sql\` beats "the migration."
|
|
417
419
|
- **Write in the vocabulary of the future question.** When you imagine asking
|
|
418
420
|
this in three months, what nouns will you use? Use those nouns. Don't
|
|
419
421
|
paraphrase the user's domain language into your own neutral summary.
|
|
@@ -430,12 +432,14 @@ extraction all see most strongly.
|
|
|
430
432
|
Measured on an 11k-engram store: 94% of tagged memories are missing at
|
|
431
433
|
least one of their own topical terms from the body, and 66% of those
|
|
432
434
|
terms never appear in the text at all.
|
|
433
|
-
- **Tags are NOT a substitute for body text.**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
- **Tags are NOT a substitute for body text.** The embedding is built from
|
|
436
|
+
\`concept + content\` only, so it never sees tags. BM25 indexes them. The
|
|
437
|
+
cross-encoder rerank passage sees ONLY \`topic=\`/\`proj=\`/\`project=\` tags, and
|
|
438
|
+
only when \`AWM_RERANK_TAGS=1\` — which \`awm setup\`, the plugin and the Desktop
|
|
439
|
+
bundle all switch on, so assume it is on — capped at 80 characters. So a word
|
|
440
|
+
that exists only as a tag is invisible to the embedding entirely, and
|
|
441
|
+
invisible to the ranker unless it is one of those three prefixes inside that
|
|
442
|
+
budget. Tag it *and* write it.
|
|
439
443
|
- **Reserve canonical for stable invariants.** Decisions, requirements,
|
|
440
444
|
hard facts, cross-agent shared context. Working class (default) is correct
|
|
441
445
|
for findings, observations, and progress notes. The canonical floor is
|
|
@@ -452,6 +456,10 @@ Every \`memory_write\` should pass these structured fields. AWM stores each as a
|
|
|
452
456
|
prefix-tag like \`proj=\`, \`topic=\`, \`intent=\`, etc. and uses them for BM25
|
|
453
457
|
and entity-bridge boosts at recall time.
|
|
454
458
|
|
|
459
|
+
(“Required” below means *required for good recall* — the MCP schema accepts every
|
|
460
|
+
one of these as optional, so a write without them succeeds and simply retrieves
|
|
461
|
+
worse.)
|
|
462
|
+
|
|
455
463
|
| Field | Required? | Format | Example |
|
|
456
464
|
|---|---|---|---|
|
|
457
465
|
| \`project\` | **YES** | one short word matching the current project | \`"EquiHub"\`, \`"AWM"\`, \`"USEA-Agent"\` |
|
|
@@ -461,12 +469,12 @@ and entity-bridge boosts at recall time.
|
|
|
461
469
|
| \`source\` | recommended | \`code-reading\` / \`debugging\` / \`discussion\` / \`research\` / \`testing\` / \`observation\` | \`"testing"\` |
|
|
462
470
|
| \`memory_class\` | when stable | \`canonical\` (source-of-truth, 0.7 floor, never staged) / \`working\` (default) / \`ephemeral\` | \`"canonical"\` |
|
|
463
471
|
| \`session_id\` | recommended | current conversation ID for entity-bridge boost | autogenerated |
|
|
464
|
-
| \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=
|
|
472
|
+
| \`tags\` | when applicable | extra prefix-tags for IDs and dates | \`["ticket=10001", "date=2026-05-11"]\` |
|
|
465
473
|
|
|
466
474
|
**Always add identifier tags when present in the content:**
|
|
467
475
|
- \`ticket=<id>\` for Freshdesk tickets
|
|
468
476
|
- \`member=<id>\` for member IDs
|
|
469
|
-
- \`
|
|
477
|
+
- \`asset=<id>\` for a domain record id
|
|
470
478
|
- \`usef=<id>\` for USEF lookups
|
|
471
479
|
- \`date=YYYY-MM-DD\` for temporal anchoring (ISO format)
|
|
472
480
|
- \`person=<Name>\` for stakeholder quotes / decisions
|
|
@@ -475,7 +483,7 @@ and entity-bridge boosts at recall time.
|
|
|
475
483
|
### Entity index — exact-match recall for named things (default off)
|
|
476
484
|
Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
|
|
477
485
|
ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
|
|
478
|
-
A query naming an entity ("ticket
|
|
486
|
+
A query naming an entity ("ticket 10003", "Dana Whitfield") can reach the memory through
|
|
479
487
|
this index even when the wording doesn't lexically match — it's a deterministic exact
|
|
480
488
|
lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
|
|
481
489
|
this reason, not just for the BM25 boost described above.
|
|
@@ -675,14 +683,14 @@ memory_write(
|
|
|
675
683
|
\`\`\`
|
|
676
684
|
memory_write(
|
|
677
685
|
concept="EquiHub period-close BLOCKED check missing server-side",
|
|
678
|
-
content="apps/web/app/(
|
|
686
|
+
content="apps/web/app/(billing)/billing/settlement-lock/page.tsx had client-only BLOCKED enforcement. Fixed by adding server-side check in SettlementService.closeRun() per schema/072-settlement-lock.sql. Without server-side check a malicious request could bypass via direct API call.",
|
|
679
687
|
project="EquiHub",
|
|
680
688
|
topic="accounting",
|
|
681
689
|
intent="finding",
|
|
682
690
|
confidence_level="verified",
|
|
683
691
|
source="debugging",
|
|
684
692
|
memory_class="canonical",
|
|
685
|
-
tags=["ticket=
|
|
693
|
+
tags=["ticket=10001", "person=Robert", "date=2026-05-11", "topic=period-close", "topic=security"]
|
|
686
694
|
)
|
|
687
695
|
\`\`\`
|
|
688
696
|
|
|
@@ -730,7 +738,7 @@ For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
|
|
|
730
738
|
multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
|
|
731
739
|
|
|
732
740
|
### Diagnostics / escape hatches (env vars, only if you know why)
|
|
733
|
-
Each
|
|
741
|
+
Each optimization is gated by an env var so it can be disabled for A/B testing if a
|
|
734
742
|
regression appears in your workload. \`memory_whoami\` prints the active fingerprint.
|
|
735
743
|
|
|
736
744
|
Retrieval (set ON by \`awm setup\` since 0.14.6 — the measured-good configuration):
|
|
@@ -752,8 +760,13 @@ Recall pipeline:
|
|
|
752
760
|
no effect on the real-store benchmark, 0.14.5).
|
|
753
761
|
- \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
|
|
754
762
|
Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
|
|
755
|
-
- \`
|
|
756
|
-
|
|
763
|
+
- \`AWM_FEEDBACK_BM25=1\` — re-enables Rocchio pseudo-relevance feedback
|
|
764
|
+
(phase 3.5), a third BM25 pass over the query plus five terms harvested from
|
|
765
|
+
the top 3 results. Default OFF since 2026-10-09: measured at 6.7–7.0% of every
|
|
766
|
+
warm recall for no measurable accuracy — 3 gold ranks moved in 950 queries
|
|
767
|
+
across three suites, two of them the wrong way. On the public corpus — the only
|
|
768
|
+
suite with recall headroom left — it is inert per query, not just in aggregate:
|
|
769
|
+
0 of 200 queries differ in rank, top-1 or score.
|
|
757
770
|
- \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
|
|
758
771
|
heuristic + LRU cache. Forces every recall through flan-t5-small.
|
|
759
772
|
|
package/src/core/embeddings.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Embedding Engine - vector embeddings via the ML worker pool.
|
|
5
5
|
*
|
|
6
|
-
* Default model: bge-small-en-v1.5 (384 dimensions,
|
|
6
|
+
* Default model: bge-small-en-v1.5 (384 dimensions, 133MB ONNX, MTEB retrieval-optimized).
|
|
7
7
|
* Configurable via AWM_EMBED_MODEL env var.
|
|
8
8
|
*
|
|
9
9
|
* AWM 0.8.x: inference dispatches through ml-worker.ts. The worker_threads
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Entity extraction for the entity inverted index (D9, 2026-07-30).
|
|
6
6
|
*
|
|
7
7
|
* v1 is deliberately conservative for precision: entities come ONLY from
|
|
8
|
-
* structured sources — prefix tags (`person=
|
|
8
|
+
* structured sources — prefix tags (`person=Avery`, `ticket=10002`) and
|
|
9
9
|
* auto-tagger `entity:` tags — never from free-text guessing. Each entity is
|
|
10
10
|
* normalized to `key:value` lowercase so lookups are exact.
|
|
11
11
|
*
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Query Expander - rewrites queries with synonyms and related terms.
|
|
5
5
|
*
|
|
6
|
-
* Uses Xenova/flan-t5-small
|
|
7
|
-
*
|
|
6
|
+
* Uses Xenova/flan-t5-small — a 141MB encoder plus a 233MB merged decoder, so
|
|
7
|
+
* 374MB of ONNX and a 377MB download with its tokenizer. By far the largest of
|
|
8
|
+
* the three models AWM fetches. Expands search queries with related terms that
|
|
9
|
+
* improve BM25 recall.
|
|
8
10
|
*
|
|
9
11
|
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
10
12
|
* — worker_threads reverted because onnxruntime-node bindings cross isolate
|
|
@@ -41,6 +41,10 @@ export const RECALL_FLAGS = [
|
|
|
41
41
|
'AWM_RERANK_POOL',
|
|
42
42
|
'AWM_RERANK_TAGS',
|
|
43
43
|
'AWM_RERANK_TAGS_LEN',
|
|
44
|
+
// Cross-encoder precision — changes every reranker score, so it changes order.
|
|
45
|
+
// Omitted until 2026-10-09, during which both arms of a dtype sweep printed the
|
|
46
|
+
// SAME arm= label; the whole point of this list is that that cannot happen.
|
|
47
|
+
'AWM_RERANKER_DTYPE',
|
|
44
48
|
// Derived retrieval text (embedding side)
|
|
45
49
|
'AWM_RETRIEVAL_TEXT',
|
|
46
50
|
// Project-dialect alias expansion
|
|
@@ -49,7 +53,8 @@ export const RECALL_FLAGS = [
|
|
|
49
53
|
// Embedding model — changes every stored vector, so it belongs here
|
|
50
54
|
'AWM_EMBED_MODEL',
|
|
51
55
|
'AWM_EMBED_DIMS',
|
|
52
|
-
|
|
56
|
+
// Rocchio pseudo-relevance feedback (phase 3.5)
|
|
57
|
+
'AWM_FEEDBACK_BM25',
|
|
53
58
|
// Spreading activation (D11 — parked)
|
|
54
59
|
'AWM_SPREAD',
|
|
55
60
|
'AWM_SPREAD_INJECT',
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recall-path telemetry — per-stage wall time for one `activate()` call.
|
|
6
|
+
*
|
|
7
|
+
* WHY THIS EXISTS
|
|
8
|
+
* ---------------
|
|
9
|
+
* The write path has had phase telemetry since D1 (`write-telemetry.ts`), but
|
|
10
|
+
* the READ path — the hot one, the one every latency claim in the docs is
|
|
11
|
+
* about — had only a single total. Every attribution of that total was
|
|
12
|
+
* therefore a reading of the code rather than a measurement, and two comments
|
|
13
|
+
* in this repository assert "the reranker is ~90% of warm recall latency"
|
|
14
|
+
* citing nothing but each other. This module exists so that number can be
|
|
15
|
+
* checked, and so a reviewer can check it too.
|
|
16
|
+
*
|
|
17
|
+
* USE
|
|
18
|
+
* ---
|
|
19
|
+
* AWM_PROFILE_RECALL=1 one stderr line per recall
|
|
20
|
+
* AWM_PROFILE_RECALL_OUT=path.jsonl append one JSON object per recall
|
|
21
|
+
*
|
|
22
|
+
* Both unset ⇒ `startRecallProfile()` returns a shared no-op and the only
|
|
23
|
+
* cost on the hot path is a handful of empty method calls.
|
|
24
|
+
*
|
|
25
|
+
* DESIGN
|
|
26
|
+
* ------
|
|
27
|
+
* Labels are FLAT and must not overlap: each `begin`/`end` pair measures a
|
|
28
|
+
* span that no other pair contains. That is what makes `unaccounted`
|
|
29
|
+
* (total minus the sum of all spans) meaningful — it is the synchronous
|
|
30
|
+
* JavaScript that no stage claims, and if it is large that is itself the
|
|
31
|
+
* finding. Repeated labels accumulate, so a per-agent loop can reuse one.
|
|
32
|
+
*
|
|
33
|
+
* Additive module: nothing here changes an existing export or code path.
|
|
34
|
+
*
|
|
35
|
+
* `node:fs` is imported statically. A lazy `require()` here was silently dead:
|
|
36
|
+
* this package is ESM, `require` is not defined, and the throw landed in the
|
|
37
|
+
* catch that exists so profiling can never break a recall — so the first run
|
|
38
|
+
* produced an empty capture and an honest-looking zero.
|
|
39
|
+
*/
|
|
40
|
+
import { appendFileSync } from 'node:fs';
|
|
41
|
+
|
|
42
|
+
const STDERR = process.env.AWM_PROFILE_RECALL === '1';
|
|
43
|
+
const OUT = process.env.AWM_PROFILE_RECALL_OUT || '';
|
|
44
|
+
const ON = STDERR || !!OUT;
|
|
45
|
+
|
|
46
|
+
export interface RecallProfiler {
|
|
47
|
+
/** Open a span. Nesting a span inside another breaks `unaccounted`. */
|
|
48
|
+
begin(label: string): void;
|
|
49
|
+
/** Close the most recent span with this label, accumulating its duration. */
|
|
50
|
+
end(label: string): void;
|
|
51
|
+
/** Record a scalar fact about this recall (pool size, counts, flags). */
|
|
52
|
+
note(key: string, value: number | string | boolean): void;
|
|
53
|
+
/** Emit the record. Safe to call once; later calls are ignored. */
|
|
54
|
+
finish(meta?: Record<string, number | string | boolean>): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NOOP: RecallProfiler = {
|
|
58
|
+
begin() {}, end() {}, note() {}, finish() {},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
class Profiler implements RecallProfiler {
|
|
62
|
+
private readonly t0 = performance.now();
|
|
63
|
+
private readonly open = new Map<string, number>();
|
|
64
|
+
private readonly sums = new Map<string, number>();
|
|
65
|
+
private readonly calls = new Map<string, number>();
|
|
66
|
+
private readonly notes: Record<string, number | string | boolean> = {};
|
|
67
|
+
private done = false;
|
|
68
|
+
|
|
69
|
+
begin(label: string): void {
|
|
70
|
+
this.open.set(label, performance.now());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
end(label: string): void {
|
|
74
|
+
const started = this.open.get(label);
|
|
75
|
+
if (started === undefined) return; // unbalanced end — ignore, never throw
|
|
76
|
+
this.open.delete(label);
|
|
77
|
+
const ms = performance.now() - started;
|
|
78
|
+
this.sums.set(label, (this.sums.get(label) ?? 0) + ms);
|
|
79
|
+
this.calls.set(label, (this.calls.get(label) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
note(key: string, value: number | string | boolean): void {
|
|
83
|
+
this.notes[key] = value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
finish(meta: Record<string, number | string | boolean> = {}): void {
|
|
87
|
+
if (this.done) return;
|
|
88
|
+
this.done = true;
|
|
89
|
+
const totalMs = performance.now() - this.t0;
|
|
90
|
+
|
|
91
|
+
const stages: Record<string, number> = {};
|
|
92
|
+
let accounted = 0;
|
|
93
|
+
for (const [label, ms] of this.sums) {
|
|
94
|
+
stages[label] = Math.round(ms * 100) / 100;
|
|
95
|
+
accounted += ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const record = {
|
|
99
|
+
totalMs: Math.round(totalMs * 100) / 100,
|
|
100
|
+
unaccountedMs: Math.round((totalMs - accounted) * 100) / 100,
|
|
101
|
+
stages,
|
|
102
|
+
calls: Object.fromEntries(this.calls),
|
|
103
|
+
...this.notes,
|
|
104
|
+
...meta,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (OUT) {
|
|
108
|
+
try {
|
|
109
|
+
appendFileSync(OUT, JSON.stringify(record) + '\n');
|
|
110
|
+
} catch { /* profiling must never break a recall */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (STDERR) {
|
|
114
|
+
const top = Object.entries(stages).sort((a, b) => b[1] - a[1]).slice(0, 6)
|
|
115
|
+
.map(([k, v]) => `${k}=${Math.round(v)}ms`).join(' ');
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`[awm] recall ${Math.round(totalMs)}ms ${top} unaccounted=${Math.round(record.unaccountedMs)}ms\n`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One profiler per `activate()` call, or a shared no-op when disabled. */
|
|
124
|
+
export function startRecallProfile(): RecallProfiler {
|
|
125
|
+
return ON ? new Profiler() : NOOP;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether recall profiling is active — for callers that want to skip setup work. */
|
|
129
|
+
export function recallProfileEnabled(): boolean {
|
|
130
|
+
return ON;
|
|
131
|
+
}
|