@unblocklabs/unblock-memory 0.3.14 → 0.3.16
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 +265 -0
- package/dist/src/abortable.d.ts +2 -0
- package/dist/src/abortable.js +21 -0
- package/dist/src/cluster-review.d.ts +47 -0
- package/dist/src/cluster-review.js +64 -0
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +25 -3
- package/dist/src/curation.js +4 -1
- package/dist/src/diagnostics.d.ts +39 -0
- package/dist/src/diagnostics.js +18 -0
- package/dist/src/evidence-review.d.ts +41 -0
- package/dist/src/evidence-review.js +50 -0
- package/dist/src/manager.d.ts +84 -4
- package/dist/src/manager.js +72 -3
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +45 -9
- package/dist/src/plugin.js +10 -20
- package/dist/src/quality-audit.d.ts +3 -0
- package/dist/src/quality-audit.js +6 -3
- package/dist/src/quality-triage.d.ts +9 -0
- package/dist/src/quality-triage.js +38 -0
- package/dist/src/response-audit.d.ts +87 -0
- package/dist/src/response-audit.js +193 -0
- package/dist/src/response-config.d.ts +13 -0
- package/dist/src/response-config.js +43 -0
- package/dist/src/response-episodes.d.ts +68 -0
- package/dist/src/response-episodes.js +242 -0
- package/dist/src/response-identity.d.ts +15 -0
- package/dist/src/response-identity.js +34 -0
- package/dist/src/response-judge.d.ts +224 -0
- package/dist/src/response-judge.js +248 -0
- package/dist/src/response-memory.d.ts +8 -0
- package/dist/src/response-memory.js +25 -0
- package/dist/src/response-outcome.d.ts +30 -0
- package/dist/src/response-outcome.js +51 -0
- package/dist/src/response-reviews.d.ts +27 -0
- package/dist/src/response-reviews.js +116 -0
- package/dist/src/response-runtime.d.ts +3 -0
- package/dist/src/response-runtime.js +150 -0
- package/dist/src/response-stages.d.ts +184 -0
- package/dist/src/response-stages.js +38 -0
- package/dist/src/response-store.d.ts +180 -0
- package/dist/src/response-store.js +411 -0
- package/dist/src/response-text.d.ts +6 -0
- package/dist/src/response-text.js +37 -0
- package/dist/src/review-tools.d.ts +5 -0
- package/dist/src/review-tools.js +116 -0
- package/dist/src/session-noise.d.ts +20 -0
- package/dist/src/session-noise.js +142 -0
- package/dist/src/session-projector.d.ts +6 -0
- package/dist/src/session-projector.js +16 -0
- package/dist/src/session-sync.d.ts +3 -1
- package/dist/src/session-sync.js +4 -1
- package/dist/src/skill-whisperer.d.ts +2 -1
- package/dist/src/skill-whisperer.js +24 -8
- package/dist/src/tool-context.d.ts +7 -0
- package/dist/src/tool-context.js +17 -0
- package/dist/src/typesafe-review.d.ts +45 -0
- package/dist/src/typesafe-review.js +134 -0
- package/openclaw.plugin.json +36 -1
- package/package.json +2 -2
- package/skills/memory-curator/SKILL.md +11 -0
- package/skills/people-whisperer/SKILL.md +7 -0
package/README.md
CHANGED
|
@@ -1,5 +1,249 @@
|
|
|
1
1
|
# Unblock Memory
|
|
2
2
|
|
|
3
|
+
## Response quality tracking (opt-in)
|
|
4
|
+
|
|
5
|
+
`responseAudit` evaluates bounded human-agent exchanges in the background. It is
|
|
6
|
+
separate from chunk-quality auditing and never changes memories or prompts. It
|
|
7
|
+
creates private response-review tasks, not memory-curation tasks.
|
|
8
|
+
Its primary purpose is tracking delivery quality over time: visible fulfillment,
|
|
9
|
+
deliverable fit, clear underdelivery and its observable reason. Memory gaps are only
|
|
10
|
+
an optional diagnostic lead, not a proxy for performance.
|
|
11
|
+
Only approved Slack sender IDs with trusted `senderKind: human` or owner metadata
|
|
12
|
+
qualify (older Slack records use unknown senderKind even for known owners).
|
|
13
|
+
Explicit bots, unverified identities, internal messages, other senders and thread changes form
|
|
14
|
+
hard boundaries. Synthetic delivery mirrors and gateway-injected answers are excluded.
|
|
15
|
+
Assistant progress messages are grouped with the terminal answer.
|
|
16
|
+
Recognized Slack envelopes are stripped even inside `upstreamUserText`; embedded
|
|
17
|
+
history is not treated as current human text. Ambiguous envelopes are excluded.
|
|
18
|
+
Removed history marks the context as limited; ordinary Markdown/JSON is preserved.
|
|
19
|
+
Human feedback closes when the next assistant turn starts. Still-open feedback,
|
|
20
|
+
no-response exchanges, incomplete/failed turns and oversized inputs are not graded.
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"responseAudit": {
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"sentimentEnabled": true,
|
|
27
|
+
"senderIds": ["YOUR_SLACK_USER_ID"],
|
|
28
|
+
"chatTypes": ["direct"],
|
|
29
|
+
"historyMessages": 6,
|
|
30
|
+
"lookbackDays": 30,
|
|
31
|
+
"maxEpisodes": 20,
|
|
32
|
+
"intervalMinutes": 60,
|
|
33
|
+
"memoryCorpora": ["memory", "knowledge"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is explicit approval to send those exchanges to TypeSafe. Sender IDs apply
|
|
39
|
+
across the agent's Slack accounts; use only identities approved in all such accounts.
|
|
40
|
+
`memoryCorpora` is optional and separately approves configured **file** corpora for
|
|
41
|
+
memory-gap investigation. Leave it empty to send no indexed memory evidence.
|
|
42
|
+
`typesafe.enabled: false` or missing credentials prevents evaluation. An interval
|
|
43
|
+
of zero means manual-only. Defaults are disabled, no approved senders, direct chats,
|
|
44
|
+
6 preceding visible messages, 30 days, 20 episodes per run and a 60-minute interval.
|
|
45
|
+
`sentimentEnabled` defaults to **true within that opt-in audit**; it does not bypass
|
|
46
|
+
approved senders or TypeSafe credentials. Set it false to omit polarity, annoyance,
|
|
47
|
+
frustration and intensity questions while retaining quality/repair judgments.
|
|
48
|
+
`intervalMinutes` controls their shared cadence; no second sentiment timer is needed.
|
|
49
|
+
The Gateway checks a durable per-agent due time on startup and every minute (no
|
|
50
|
+
agent-turn cron or separate launchd job). First enablement waits one interval;
|
|
51
|
+
restarts preserve the due time and an overdue schedule gets one bounded catch-up,
|
|
52
|
+
not one run per missed interval. Each attempt advances the due time before work,
|
|
53
|
+
including missing-key skips, failures or interrupted runs, to prevent retry storms.
|
|
54
|
+
Changing the interval recalculates the due time from the last scheduled attempt
|
|
55
|
+
(or initial enablement). The Gateway must be running; manual audits do not change
|
|
56
|
+
the automatic schedule. Missing/unreadable credentials skip all quality and sentiment
|
|
57
|
+
inference without failing Gateway startup or normal memory functionality.
|
|
58
|
+
Changing the interval does not invalidate cached results. Changing the sentiment
|
|
59
|
+
toggle selects a separate reporting cohort, so older missing sentiment is not
|
|
60
|
+
treated as neutral; unchanged quality/feedback stages are reused across the toggle.
|
|
61
|
+
|
|
62
|
+
Operator commands (not agent tools):
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
openclaw memory-responses audit --agent main --dry-run
|
|
66
|
+
openclaw memory-responses audit --agent main
|
|
67
|
+
openclaw memory-responses report --agent main
|
|
68
|
+
openclaw memory-responses report --agent main --episode EPISODE_ID
|
|
69
|
+
openclaw memory-responses report --agent main --sender SLACK_USER_ID --account ACCOUNT_SCOPE --bucket day --since 2026-09-01 --until 2026-10-01
|
|
70
|
+
openclaw memory-responses report --agent main --person PERSON_ID
|
|
71
|
+
openclaw memory-responses tasks --agent main
|
|
72
|
+
openclaw memory-responses review --agent main --id TASK_ID --status deferred --reviewer human --note "Review the linked exchanges before changing preferences"
|
|
73
|
+
openclaw memory-responses annotate --agent main --date 2026-09-18 --kind prompt --note "Known prompt revision deployed"
|
|
74
|
+
openclaw memory-responses retry-failed --agent main
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Reports group by scoped human identity as well as task/model/time. Names are not
|
|
78
|
+
identity keys. Existing active people-store links are resolved read-only at assessment
|
|
79
|
+
time; missing links do not prevent analysis. Unknown account scopes stay isolated
|
|
80
|
+
per session. No new identity fields are sent to TypeSafe. Date ranges are UTC with
|
|
81
|
+
an inclusive start and exclusive end. `periodStart` identifies a day/week bucket;
|
|
82
|
+
legacy `week`/`fromWeek`/`toWeek` fields remain aliases. `--task-type` and `--model`
|
|
83
|
+
further narrow comparisons. Human-specific scores are not rankings of the humans:
|
|
84
|
+
task difficulty, feedback habits and selection bias remain important.
|
|
85
|
+
|
|
86
|
+
Session checkpoints hash bounded active source bytes; unchanged sessions skip
|
|
87
|
+
extraction and all inference. Changed sessions are re-extracted within the existing
|
|
88
|
+
budget, then stage hashes reuse unchanged quality, feedback, sentiment and later
|
|
89
|
+
evidence judgments. Only hashes/counts are checkpointed, never a transcript copy.
|
|
90
|
+
A persisted cursor rotates through discovery and tracked-session reconciliation;
|
|
91
|
+
`deferredByLimit` includes known backlog and a lower-bound marker for unvisited
|
|
92
|
+
sessions. `stages` exposes pending/failed/succeeded counts and exhausted retries
|
|
93
|
+
for the cohort/date range, before person filters. `retry-failed` only resets failed
|
|
94
|
+
work; successful stages remain cached. Source freshness is checked before activation.
|
|
95
|
+
|
|
96
|
+
Review tasks distinguish concrete delivery shortfalls from high-intensity human
|
|
97
|
+
experience complaints. Stable task keys include exchange, scoped human and issue
|
|
98
|
+
family. Decisions survive rescoring; stale source evidence and superseded findings
|
|
99
|
+
are labeled separately. Review status/provenance never changes the raw judgments.
|
|
100
|
+
Tasks and change annotations are operator-only and stay out of memory/whisperer
|
|
101
|
+
prompts. `--reviewer` records human/agent provenance, not authentication or a new
|
|
102
|
+
permission grant. Task lists disclose their 1,000-item cap. There are no automatic
|
|
103
|
+
dossier updates: review the evidence and approve any concrete preference separately.
|
|
104
|
+
Old cohorts remain stored; the first staged-cohort run does not silently import
|
|
105
|
+
unverified older rubric judgments. Audit-history retention is not automatic.
|
|
106
|
+
|
|
107
|
+
Two separate TypeSafe requests prevent human feedback from influencing the original
|
|
108
|
+
fulfillment/deliverable-fit grade. The feedback pass distinguishes acceptance,
|
|
109
|
+
correction, continuation, unrelated replies, expressed sentiment, repeated constraints
|
|
110
|
+
and avoidable rework. Current-index memory investigation runs only for a strong
|
|
111
|
+
memory-gap signal: lexical retrieval selects up to three whole short documents from
|
|
112
|
+
approved collections. This is an investigation lead, **not proof of historical
|
|
113
|
+
availability, factual truth, or agent fault**. Tool-call counts do not establish what
|
|
114
|
+
the model saw or whether it should have searched. Unseen artifacts are unassessable.
|
|
115
|
+
|
|
116
|
+
Deliverable kind/format/scope has its own assessability gate, independent of whether
|
|
117
|
+
execution or external facts can be verified. Feedback attribution distinguishes the
|
|
118
|
+
current answer, earlier behavior, delivery, missing proactive action, external events,
|
|
119
|
+
new work and mixed/unclear targets. A reported forgotten instruction does not prove
|
|
120
|
+
searchable memory existed. A third, separate request examines the original exchange,
|
|
121
|
+
human feedback and available next assistant block for specific reported shortfalls,
|
|
122
|
+
acknowledgment, explicit factual corrections, delivery failures and regressions. These are
|
|
123
|
+
retrospective signals, not independently verified facts and never inputs to the
|
|
124
|
+
original grade. Clean text preceding a synthetic error/delivery notice can be assessed
|
|
125
|
+
as **partial** evidence; the notice itself is excluded and no successful completion
|
|
126
|
+
is inferred. Later evidence is capped at six messages/12K characters; incomplete,
|
|
127
|
+
unsafe or oversized blocks stay explicitly pending/unavailable/oversized. New later
|
|
128
|
+
evidence changes the input hash; only changed assessment stages are re-evaluated,
|
|
129
|
+
within normal audit budgets. Successful stages survive failures in later stages.
|
|
130
|
+
When the next block is unavailable, the third pass uses only the original exchange
|
|
131
|
+
and feedback; it cannot infer a missing delivery from missing later evidence.
|
|
132
|
+
|
|
133
|
+
Code combines narrow, confident evidence into an **observed outcome**, preserving
|
|
134
|
+
its basis and reason. A concrete original-answer shortfall or later admission takes
|
|
135
|
+
precedence over praise. Broad reported failures are used only when they do not
|
|
136
|
+
depend on a newly introduced requirement. Accurate explanations of earlier mistakes,
|
|
137
|
+
ordinary follow-ups, necessary clarification and unseen work are not automatically
|
|
138
|
+
failures. Sentiment and earlier-workflow complaints remain separate review signals.
|
|
139
|
+
Sentiment includes independent annoyance and frustration yes-probabilities (both
|
|
140
|
+
can apply), plus an expressed-dissatisfaction intensity score from 0 to 3. Intensity
|
|
141
|
+
means no expressed displeasure / restrained displeasure / pointed complaint /
|
|
142
|
+
explicit rejection or loss of trust. It is **not confidence or failure severity**.
|
|
143
|
+
External frustration, brevity and factual corrections alone do not establish
|
|
144
|
+
annoyance or frustration; mixed praise and complaints can still carry both signals.
|
|
145
|
+
Weekly reports show dissatisfaction, annoyance and frustration rates, intensity
|
|
146
|
+
means, unknown counts and their own assessment denominators. Unassessed results
|
|
147
|
+
are never counted as neutral. Sentiment deltas require 20 samples in both periods
|
|
148
|
+
and matching assessment coverage; they remain descriptive, not causal evidence.
|
|
149
|
+
Outcome, evidence basis and failure reasons remain distinct: a correction does not
|
|
150
|
+
automatically mean `incorrect_claim`. Confident reason judgments and direct
|
|
151
|
+
delivery/regression admissions supply reasons; otherwise `reasonStatus` is
|
|
152
|
+
`uncertain`. `reasonDetails` retain each label's source and strength, distinguishing
|
|
153
|
+
Choice confidence from Noul yes-probability. Multiple supported reasons can coexist.
|
|
154
|
+
`reportVersion` identifies composition/reporting semantics independently of the
|
|
155
|
+
judge rubric, allowing cached judgments to be re-reported without re-inference.
|
|
156
|
+
|
|
157
|
+
Results live in the agent's private `unblock-memory/response-audit.sqlite`, outside
|
|
158
|
+
the memory index. It stores judgments and source event references/hashes, not copies
|
|
159
|
+
of conversations. Identical successful inputs are cached; source rewrites invalidate
|
|
160
|
+
in-scope results on the next scan. Reports partition by fixed judge/rubric/context
|
|
161
|
+
configuration, UTC week, task type and agent model. They expose eligible/assessed
|
|
162
|
+
counts, excluded cases, confidence-qualified score means with per-dimension denominators, rework rates with Wilson
|
|
163
|
+
intervals, and evidence IDs. Small groups (<20) are marked explicitly. Confidence
|
|
164
|
+
thresholds are provisional, not calibrated guarantees. Human-reviewed evaluation
|
|
165
|
+
data is still needed before drawing performance conclusions.
|
|
166
|
+
Reports include dated clear-underdelivery examples and reason counts. Descriptive
|
|
167
|
+
score deltas compare successive available UTC weeks within the same task type,
|
|
168
|
+
agent model and rubric/configuration, with at least 20 confident scores per dimension
|
|
169
|
+
in each period and unchanged scored coverage; changed coverage withholds the score
|
|
170
|
+
delta. Outcome trends show acknowledgment, reported-shortfall and unknown rates
|
|
171
|
+
against **all evaluated exchanges**, with at least 20 evaluated exchanges per period.
|
|
172
|
+
Read the three rates together: fewer acknowledgments can mean more unknowns, not
|
|
173
|
+
more failures. Every delta includes before/after values, sample counts, denominator
|
|
174
|
+
and coverage-change flags. Unknown task types/models cannot produce deltas. These
|
|
175
|
+
are not statistical change-point detections or proof of causality; model/version
|
|
176
|
+
changes remain visible as separate groups rather than silently mixing cohorts.
|
|
177
|
+
The legacy `observedSuccessRate` group field remains acknowledgment / known outcomes
|
|
178
|
+
for compatibility, but is not used for trends. Unknowns are never successes.
|
|
179
|
+
Coverage changes and threshold variability can move rates; acknowledgment is not
|
|
180
|
+
factual verification. Week buckets
|
|
181
|
+
may be partial, and several exchanges in one session are not independent. Wilson
|
|
182
|
+
intervals are descriptive, not calibrated confidence about overall agent ability.
|
|
183
|
+
|
|
184
|
+
Each run selects at most 100 recent sessions for inference, each at most 2,000 active events/2M
|
|
185
|
+
characters; episodes must fit 24K characters and six feedback messages without
|
|
186
|
+
truncating the answer. Coverage counts describe the scanned sessions; only episodes
|
|
187
|
+
within `lookbackDays` are judged. Caps, failures and no-feedback cases remain visible.
|
|
188
|
+
Saved sessions in the report window are also reconciled independently of that
|
|
189
|
+
selection, so removing an entire active branch retires its scores. Oversized saved
|
|
190
|
+
sessions defer reconciliation rather than being treated as deleted; the report
|
|
191
|
+
exposes `reconciledSessions` and `reconciliationDeferred`. All reconciliation shares
|
|
192
|
+
the run deadline. Freshness checks compare the assessed episode, not unrelated
|
|
193
|
+
later session activity. Actual snapshot races do not exhaust provider retries.
|
|
194
|
+
The whole run has a two-minute deadline, at most three provider attempts per input (ten-minute
|
|
195
|
+
backoff), and a cross-process lease. Scheduling never starts inference on the agent
|
|
196
|
+
turn path or boots a QMD manager. No model downloads or source re-indexing occur.
|
|
197
|
+
The report is observational: different task mixes, selective human replies and judge
|
|
198
|
+
changes can produce apparent trends. It does not automatically declare regressions,
|
|
199
|
+
rewrite prompts, or treat silence as success.
|
|
200
|
+
|
|
201
|
+
## Review and diagnostics
|
|
202
|
+
|
|
203
|
+
- `memory_diagnostics` reports credential **availability only**, per-agent process-local
|
|
204
|
+
whisperer counters, projection version, old indexed-session projection count, and
|
|
205
|
+
embedding readiness. Counters are bounded to 100 agents and reset on restart.
|
|
206
|
+
No prompts, excerpts, paths, keys, or provider error bodies enter these counters.
|
|
207
|
+
Parser cleanup/budget-skip counts are persisted with the latest completed
|
|
208
|
+
`memory_sync_status`; unchanged sessions are not counted again. QMD structural
|
|
209
|
+
omission counts cover this manager's embedding passes, not the whole corpus.
|
|
210
|
+
- Quality-audit groups distinguish `preserve_evidence_repair`, `inspect_scaffolding`,
|
|
211
|
+
and `context_review`, reusing cached noise/evidence judgments without another call.
|
|
212
|
+
Evidence-preserving repair tasks sort first. Maintenance tasks expose indexed
|
|
213
|
+
fingerprint presence; `not_present_in_index` is **not** a verified repair and
|
|
214
|
+
never resolves or deletes the task. Chunk boundaries may simply have changed.
|
|
215
|
+
- `memory_review_cluster` uses the existing `qualityAudit` opt-in/corpus allowlist.
|
|
216
|
+
It judges up to three representative and three low-membership members, deduplicates
|
|
217
|
+
the sample, and skips unapproved or >2,000-character chunks whole. Repeated defect
|
|
218
|
+
labels are investigation leads only. Stale/changed samples are rejected; useful
|
|
219
|
+
or uncertain members are retained. No tasks or sources are modified.
|
|
220
|
+
- `memory_review_claim` accepts one atomic claim (up to 2,000 characters) and 1–3
|
|
221
|
+
citations `{path, from, lines}`. It reads approved indexed evidence itself (at
|
|
222
|
+
most 6,000 characters), returns supports/contradicts/insufficient_evidence with
|
|
223
|
+
confidence and source hashes, and never writes or authorizes a write. Support
|
|
224
|
+
below 0.9 confidence is marked for review. This threshold is provisional, not a
|
|
225
|
+
guarantee of truth; read original evidence and verify current-state claims.
|
|
226
|
+
|
|
227
|
+
New optional configuration (corpora must already be configured):
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"evidenceReview": { "enabled": true, "corpora": ["memory", "knowledge", "sessions"] },
|
|
232
|
+
"memoryWhisperer": {
|
|
233
|
+
"enabled": true, "corpora": ["memory", "knowledge"], "complementaryHints": true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Both additions default off. Claim review sends the proposed claim and approved
|
|
239
|
+
source excerpts to TypeSafe; cluster review sends approved sampled excerpts.
|
|
240
|
+
Complementary hints use one extra bounded call over at most four already-useful
|
|
241
|
+
candidates (six directional comparisons). Only redundancy probability >=0.9
|
|
242
|
+
removes a hint; distinct evidence and contradictions should remain. Provider errors
|
|
243
|
+
retain baseline hints, while the existing total turn deadline/cancellation still
|
|
244
|
+
suppresses late results. Missing keys or disabled TypeSafe never enable these calls.
|
|
245
|
+
The retrieval corpus/session boundaries are unchanged.
|
|
246
|
+
|
|
3
247
|
Workspace-native memory for OpenClaw, powered internally by `@unblocklabs/qmd`.
|
|
4
248
|
It keeps one warm QMD store per agent and exposes the standard `memory_search`
|
|
5
249
|
and `memory_get` tools. Search uses semantic chunking and direct QMD vector
|
|
@@ -34,6 +278,27 @@ remain opt-in and should stay outside file-corpus globs (new default:
|
|
|
34
278
|
messages. Truncated sessions stay explicitly incomplete; enabling archive
|
|
35
279
|
enrichment is not part of this version.
|
|
36
280
|
|
|
281
|
+
### Conservative ingestion cleanup
|
|
282
|
+
|
|
283
|
+
Session projections unwrap complete, recognized task/attachment envelopes while
|
|
284
|
+
keeping the actual result, task/status, filename, MIME type, and untrusted-content
|
|
285
|
+
label. Internal task cleanup requires structured inter-session provenance, not
|
|
286
|
+
just matching text. Unknown formats, malformed envelopes, and code examples stay
|
|
287
|
+
intact. Assistant messages and Loggie's separate projection path are unaffected.
|
|
288
|
+
Raw session events and workspace memory files are never rewritten.
|
|
289
|
+
Attachment matching has a fixed work budget; oversized or repeatedly nested/
|
|
290
|
+
incomplete envelopes leave the entire message unchanged rather than blocking sync.
|
|
291
|
+
|
|
292
|
+
The companion QMD semantic-chunking update skips only source-confirmed standalone
|
|
293
|
+
REM heading/marker spans and orphan closing fences. Reflections and useful text
|
|
294
|
+
remain searchable, with original source offsets. These are deterministic rules,
|
|
295
|
+
not TypeSafe judgments; audit flags never authorize automatic memory deletion.
|
|
296
|
+
|
|
297
|
+
This release pins QMD 2.9.6. Projector/chunker version changes refresh derived
|
|
298
|
+
projections and embeddings on their next normal sync; the first sync may take
|
|
299
|
+
longer while re-embedding. No manual deletion of source memories or review tasks
|
|
300
|
+
is needed.
|
|
301
|
+
|
|
37
302
|
## Installation
|
|
38
303
|
|
|
39
304
|
From npm:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Stop waiting without cancelling shared work that other callers still need. */
|
|
2
|
+
export async function abortable(pending, signal) {
|
|
3
|
+
if (!signal)
|
|
4
|
+
return pending;
|
|
5
|
+
let onAbort = () => { };
|
|
6
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
7
|
+
onAbort = () => reject(signal.reason);
|
|
8
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9
|
+
if (signal.aborted)
|
|
10
|
+
onAbort();
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
// Observe late failures even when cancellation wins the race.
|
|
14
|
+
const result = await Promise.race([pending, cancelled]);
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
signal.removeEventListener("abort", onAbort);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
+
import { type ResolvedSource } from "./sources.js";
|
|
3
|
+
/** Inspect center and edge samples; never extrapolate their labels to the rest of a cluster. */
|
|
4
|
+
export declare function reviewClusterIngestion(params: {
|
|
5
|
+
db: QMDStore["internal"]["db"];
|
|
6
|
+
sources: readonly ResolvedSource[];
|
|
7
|
+
clusterId: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
read?: <T>(run: () => T) => Promise<T>;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
status: "unavailable";
|
|
14
|
+
reason: string;
|
|
15
|
+
sample?: undefined;
|
|
16
|
+
considered?: undefined;
|
|
17
|
+
runId?: undefined;
|
|
18
|
+
clusterSize?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
status: "ok";
|
|
21
|
+
runId: string;
|
|
22
|
+
clusterId: string;
|
|
23
|
+
members: {
|
|
24
|
+
flagged: boolean;
|
|
25
|
+
defect: "encoding" | "wrapper" | "boilerplate" | "none_or_uncertain";
|
|
26
|
+
confidence: number;
|
|
27
|
+
path: string;
|
|
28
|
+
hash: string;
|
|
29
|
+
seq: number;
|
|
30
|
+
from: number;
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
}[];
|
|
33
|
+
recurring: {
|
|
34
|
+
defect: string;
|
|
35
|
+
examples: {
|
|
36
|
+
path: string;
|
|
37
|
+
from: number;
|
|
38
|
+
fingerprint: string;
|
|
39
|
+
}[];
|
|
40
|
+
}[];
|
|
41
|
+
sampled: number;
|
|
42
|
+
considered: number;
|
|
43
|
+
clusterSize: number | undefined;
|
|
44
|
+
policy: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
reason?: undefined;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readAnalysisSummary, readCluster } from "./analysis.js";
|
|
2
|
+
import { parseSafeVirtualPath } from "./sources.js";
|
|
3
|
+
import { reviewClusterDefects } from "./typesafe-review.js";
|
|
4
|
+
import { chunkFingerprint } from "./curation.js";
|
|
5
|
+
/** Inspect center and edge samples; never extrapolate their labels to the rest of a cluster. */
|
|
6
|
+
export async function reviewClusterIngestion(params) {
|
|
7
|
+
params.signal.throwIfAborted();
|
|
8
|
+
const sources = new Map(params.sources.filter(source => source.kind !== "skills").map(source => [source.collection, source]));
|
|
9
|
+
const read = params.read ?? (async (run) => run());
|
|
10
|
+
const snapshot = await read(() => {
|
|
11
|
+
const center = readCluster(params.db, params.clusterId, 3, 0, "representative");
|
|
12
|
+
if (center.status !== "ok" || center.stale)
|
|
13
|
+
return { status: "unavailable", reason: "Cluster missing or stale; refresh analysis first" };
|
|
14
|
+
const edge = readCluster(params.db, params.clusterId, 3, 0, "score_asc");
|
|
15
|
+
const candidates = [...new Map([...(center.members ?? []), ...(edge.members ?? [])].map(member => [`${member.hash}:${member.seq}`, member])).values()];
|
|
16
|
+
const sample = candidates.flatMap(member => {
|
|
17
|
+
const path = member.sourcePaths.find(path => parseSafeVirtualPath(path, sources));
|
|
18
|
+
if (!path)
|
|
19
|
+
return [];
|
|
20
|
+
const safe = parseSafeVirtualPath(path, sources);
|
|
21
|
+
// Reload the full chunk; analysis previews may be truncated. Never judge a silently cut prefix.
|
|
22
|
+
const row = params.db.prepare(`SELECT c.doc, v.pos, v.chunk_len FROM documents d JOIN content c ON c.hash = d.hash
|
|
23
|
+
JOIN content_vectors v ON v.hash = d.hash WHERE d.active = 1 AND d.collection = ? AND d.path = ? AND d.hash = ? AND v.seq = ?`)
|
|
24
|
+
.get(safe.source.collection, safe.relativePath, member.hash, member.seq);
|
|
25
|
+
if (!row || row.pos < 0 || row.chunk_len < 1 || row.pos + row.chunk_len > row.doc.length || row.chunk_len > 2000)
|
|
26
|
+
return [];
|
|
27
|
+
const text = row.doc.slice(row.pos, row.pos + row.chunk_len);
|
|
28
|
+
return [{ path, hash: member.hash, seq: member.seq, from: row.doc.slice(0, row.pos).split("\n").length,
|
|
29
|
+
fingerprint: chunkFingerprint(text), text }];
|
|
30
|
+
});
|
|
31
|
+
return { status: "ready", sample, considered: candidates.length, runId: center.runId, clusterSize: center.cluster?.availableSize };
|
|
32
|
+
});
|
|
33
|
+
if (snapshot.status !== "ready")
|
|
34
|
+
return snapshot;
|
|
35
|
+
const { sample } = snapshot;
|
|
36
|
+
if (!sample.length)
|
|
37
|
+
return { status: "unavailable", reason: "No complete bounded members in approved corpora" };
|
|
38
|
+
const judgments = await reviewClusterDefects({ ...params, excerpts: sample.map(member => member.text) });
|
|
39
|
+
params.signal.throwIfAborted();
|
|
40
|
+
return read(() => {
|
|
41
|
+
const current = readAnalysisSummary(params.db);
|
|
42
|
+
if (!current || current.stale || current.runId !== snapshot.runId)
|
|
43
|
+
return { status: "unavailable", reason: "Analysis changed during review; retry" };
|
|
44
|
+
for (const member of sample) {
|
|
45
|
+
const safe = parseSafeVirtualPath(member.path, sources);
|
|
46
|
+
if (!safe)
|
|
47
|
+
return { status: "unavailable", reason: "Source scope changed; retry" };
|
|
48
|
+
const row = params.db.prepare(`SELECT c.doc, v.pos, v.chunk_len FROM documents d JOIN content c ON c.hash = d.hash
|
|
49
|
+
JOIN content_vectors v ON v.hash = d.hash WHERE d.active = 1 AND d.collection = ? AND d.path = ? AND d.hash = ? AND v.seq = ?`)
|
|
50
|
+
.get(safe.source.collection, safe.relativePath, member.hash, member.seq);
|
|
51
|
+
if (!row || chunkFingerprint(row.doc.slice(row.pos, row.pos + row.chunk_len)) !== member.fingerprint)
|
|
52
|
+
return { status: "unavailable", reason: "Sample changed during review; retry" };
|
|
53
|
+
}
|
|
54
|
+
const members = sample.map(({ text: _text, ...member }, index) => ({ ...member, ...judgments[index],
|
|
55
|
+
flagged: judgments[index].defect !== "none_or_uncertain" && judgments[index].confidence >= 0.9 }));
|
|
56
|
+
const recurring = ["wrapper", "encoding", "boilerplate"].flatMap(defect => {
|
|
57
|
+
const examples = members.filter(member => member.flagged && member.defect === defect);
|
|
58
|
+
return examples.length >= 2 ? [{ defect, examples: examples.map(member => ({ path: member.path, from: member.from, fingerprint: member.fingerprint })) }] : [];
|
|
59
|
+
});
|
|
60
|
+
return { status: "ok", runId: snapshot.runId, clusterId: params.clusterId, members, recurring,
|
|
61
|
+
sampled: sample.length, considered: snapshot.considered, clusterSize: snapshot.clusterSize,
|
|
62
|
+
policy: "jev-1.13.0:cluster-defects-v1", scope: "Center/edge sample of approved complete chunks only. Recurring labels are hypotheses, not proof of a shared cause or permission to change any member. Unreviewed members remain unknown." };
|
|
63
|
+
});
|
|
64
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResponseAuditConfig } from "./response-config.js";
|
|
1
2
|
export type FileCorpusConfig = {
|
|
2
3
|
name: string;
|
|
3
4
|
kind: "files";
|
|
@@ -36,6 +37,11 @@ export type UnblockMemoryConfig = {
|
|
|
36
37
|
corpora: readonly string[];
|
|
37
38
|
minNoise: number;
|
|
38
39
|
};
|
|
40
|
+
evidenceReview: {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
corpora: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
responseAudit: ResponseAuditConfig;
|
|
39
45
|
people: {
|
|
40
46
|
enabled: boolean;
|
|
41
47
|
whisperer: {
|
|
@@ -54,6 +60,7 @@ export type UnblockMemoryConfig = {
|
|
|
54
60
|
};
|
|
55
61
|
memoryWhisperer: {
|
|
56
62
|
enabled: boolean;
|
|
63
|
+
complementaryHints: boolean;
|
|
57
64
|
corpora: readonly string[];
|
|
58
65
|
historyMessages: number;
|
|
59
66
|
minUsefulness: number;
|
package/dist/src/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
+
import { resolveResponseAudit } from "./response-config.js";
|
|
2
3
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
3
4
|
const DEFAULT_SESSION_MAX_EXPANDED_TOKENS = 500;
|
|
4
5
|
const MAX_SESSION_MAX_EXPANDED_TOKENS = 10_000;
|
|
@@ -79,7 +80,7 @@ const DEFAULT_SKILL_WHISPERER = {
|
|
|
79
80
|
cooldownTurns: 10,
|
|
80
81
|
};
|
|
81
82
|
const DEFAULT_MEMORY_WHISPERER = {
|
|
82
|
-
enabled: false, corpora: [], historyMessages: 5, minUsefulness: 0.9,
|
|
83
|
+
enabled: false, complementaryHints: false, corpora: [], historyMessages: 5, minUsefulness: 0.9,
|
|
83
84
|
maxHints: 2, cooldownTurns: 10, timeoutMs: 3000,
|
|
84
85
|
};
|
|
85
86
|
function resolveMemoryWhisperer(value, corpora) {
|
|
@@ -93,6 +94,9 @@ function resolveMemoryWhisperer(value, corpora) {
|
|
|
93
94
|
const enabled = config.enabled ?? false;
|
|
94
95
|
if (typeof enabled !== "boolean")
|
|
95
96
|
throw new Error("unblock-memory memoryWhisperer.enabled must be a boolean");
|
|
97
|
+
const complementaryHints = config.complementaryHints ?? false;
|
|
98
|
+
if (typeof complementaryHints !== "boolean")
|
|
99
|
+
throw new Error("memoryWhisperer.complementaryHints must be a boolean");
|
|
96
100
|
const selected = config.corpora ?? [];
|
|
97
101
|
if (!Array.isArray(selected) || !selected.every((name) => typeof name === "string" && corpora.some(corpus => corpus.name === name && corpus.kind !== "skills"))) {
|
|
98
102
|
throw new Error("unblock-memory memoryWhisperer.corpora must list configured non-skill corpora");
|
|
@@ -112,7 +116,7 @@ function resolveMemoryWhisperer(value, corpora) {
|
|
|
112
116
|
throw new Error("unblock-memory memoryWhisperer.minUsefulness must be between 0 and 1");
|
|
113
117
|
}
|
|
114
118
|
return {
|
|
115
|
-
enabled, corpora: [...new Set(selected)], historyMessages, cooldownTurns, minUsefulness,
|
|
119
|
+
enabled, complementaryHints, corpora: [...new Set(selected)], historyMessages, cooldownTurns, minUsefulness,
|
|
116
120
|
maxHints: positiveInteger(config.maxHints, 2, "memoryWhisperer.maxHints", 2),
|
|
117
121
|
timeoutMs: positiveInteger(config.timeoutMs, 3000, "memoryWhisperer.timeoutMs", 10_000),
|
|
118
122
|
};
|
|
@@ -258,6 +262,8 @@ export function resolveConfig(value) {
|
|
|
258
262
|
analysis: {},
|
|
259
263
|
typesafe: { ...DEFAULT_TYPESAFE_CONFIG },
|
|
260
264
|
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
265
|
+
evidenceReview: { enabled: false, corpora: [] },
|
|
266
|
+
responseAudit: resolveResponseAudit(undefined, DEFAULT_CORPORA),
|
|
261
267
|
people: DEFAULT_PEOPLE_CONFIG,
|
|
262
268
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
263
269
|
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER },
|
|
@@ -267,9 +273,23 @@ export function resolveConfig(value) {
|
|
|
267
273
|
throw new Error("unblock-memory config must be an object");
|
|
268
274
|
}
|
|
269
275
|
const config = value;
|
|
270
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit"], "config");
|
|
276
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
|
|
271
277
|
const corpora = resolveCorpora(config.corpora);
|
|
272
278
|
const people = resolvePeople(config.people);
|
|
279
|
+
let evidenceReview = { enabled: false, corpora: [] };
|
|
280
|
+
if (config.evidenceReview !== undefined) {
|
|
281
|
+
const value = config.evidenceReview;
|
|
282
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
283
|
+
throw new Error("evidenceReview must be an object");
|
|
284
|
+
assertOnlyKeys(value, ["enabled", "corpora"], "evidenceReview");
|
|
285
|
+
try {
|
|
286
|
+
const approved = resolveQualityAudit(value, corpora);
|
|
287
|
+
evidenceReview = { enabled: approved.enabled, corpora: approved.corpora };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new Error("evidenceReview requires a boolean enabled and explicit configured non-skill corpora when enabled");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
273
293
|
if (config.keepEmbeddingModelWarm !== undefined &&
|
|
274
294
|
typeof config.keepEmbeddingModelWarm !== "boolean") {
|
|
275
295
|
throw new Error("unblock-memory keepEmbeddingModelWarm must be a boolean");
|
|
@@ -328,5 +348,7 @@ export function resolveConfig(value) {
|
|
|
328
348
|
}
|
|
329
349
|
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer,
|
|
330
350
|
qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
|
|
351
|
+
evidenceReview,
|
|
352
|
+
responseAudit: resolveResponseAudit(config.responseAudit, corpora),
|
|
331
353
|
memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora), typesafe: resolveTypeSafe(config.typesafe) };
|
|
332
354
|
}
|
package/dist/src/curation.js
CHANGED
|
@@ -155,7 +155,10 @@ export class CurationStore {
|
|
|
155
155
|
return this.#db.prepare(`
|
|
156
156
|
SELECT * FROM maintenance_tasks
|
|
157
157
|
WHERE status = ?
|
|
158
|
-
ORDER BY
|
|
158
|
+
ORDER BY CASE WHEN type = 'quality_review' AND json_valid(detail) THEN
|
|
159
|
+
CASE WHEN json_extract(detail, '$.evidence') >= 0.8 AND
|
|
160
|
+
(json_extract(detail, '$.noise') >= 0.8 OR reason = 'possible_double_encoded_message') THEN 0 ELSE 1 END
|
|
161
|
+
ELSE 1 END, created_at, id
|
|
159
162
|
LIMIT ?
|
|
160
163
|
`).all(status, limit).map((row) => task(row));
|
|
161
164
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Whisperer = "skill" | "memory";
|
|
2
|
+
type Outcome = "missing_key" | "typesafe_disabled" | "no_candidates" | "rejected" | "cooldown" | "emitted" | "failed" | "timed_out" | "cancelled" | "unavailable" | "payload_limit" | "redundancy_unavailable";
|
|
3
|
+
/** Process-local, content-free and bounded. Agent IDs are keys, never included in snapshots. */
|
|
4
|
+
export declare class WhispererDiagnostics {
|
|
5
|
+
#private;
|
|
6
|
+
record(agentId: string, whisperer: Whisperer, outcome: Outcome): void;
|
|
7
|
+
snapshot(agentId: string): {
|
|
8
|
+
skill: {
|
|
9
|
+
unavailable?: number | undefined;
|
|
10
|
+
rejected?: number | undefined;
|
|
11
|
+
failed?: number | undefined;
|
|
12
|
+
missing_key?: number | undefined;
|
|
13
|
+
typesafe_disabled?: number | undefined;
|
|
14
|
+
no_candidates?: number | undefined;
|
|
15
|
+
cooldown?: number | undefined;
|
|
16
|
+
emitted?: number | undefined;
|
|
17
|
+
timed_out?: number | undefined;
|
|
18
|
+
cancelled?: number | undefined;
|
|
19
|
+
payload_limit?: number | undefined;
|
|
20
|
+
redundancy_unavailable?: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
memory: {
|
|
23
|
+
unavailable?: number | undefined;
|
|
24
|
+
rejected?: number | undefined;
|
|
25
|
+
failed?: number | undefined;
|
|
26
|
+
missing_key?: number | undefined;
|
|
27
|
+
typesafe_disabled?: number | undefined;
|
|
28
|
+
no_candidates?: number | undefined;
|
|
29
|
+
cooldown?: number | undefined;
|
|
30
|
+
emitted?: number | undefined;
|
|
31
|
+
timed_out?: number | undefined;
|
|
32
|
+
cancelled?: number | undefined;
|
|
33
|
+
payload_limit?: number | undefined;
|
|
34
|
+
redundancy_unavailable?: number | undefined;
|
|
35
|
+
};
|
|
36
|
+
scope: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Process-local, content-free and bounded. Agent IDs are keys, never included in snapshots. */
|
|
2
|
+
export class WhispererDiagnostics {
|
|
3
|
+
#agents = new Map();
|
|
4
|
+
record(agentId, whisperer, outcome) {
|
|
5
|
+
let entry = this.#agents.get(agentId);
|
|
6
|
+
if (!entry) {
|
|
7
|
+
if (this.#agents.size >= 100)
|
|
8
|
+
this.#agents.delete(this.#agents.keys().next().value);
|
|
9
|
+
entry = { skill: {}, memory: {} };
|
|
10
|
+
this.#agents.set(agentId, entry);
|
|
11
|
+
}
|
|
12
|
+
entry[whisperer][outcome] = Math.min(Number.MAX_SAFE_INTEGER, (entry[whisperer][outcome] ?? 0) + 1);
|
|
13
|
+
}
|
|
14
|
+
snapshot(agentId) {
|
|
15
|
+
const entry = this.#agents.get(agentId);
|
|
16
|
+
return { skill: { ...entry?.skill }, memory: { ...entry?.memory }, scope: "process lifetime; up to 100 agents" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
+
import { type ResolvedSource } from "./sources.js";
|
|
3
|
+
export type EvidenceCitation = {
|
|
4
|
+
path: string;
|
|
5
|
+
from: number;
|
|
6
|
+
lines: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function reviewIndexedClaim(params: {
|
|
9
|
+
db: QMDStore["internal"]["db"];
|
|
10
|
+
sources: readonly ResolvedSource[];
|
|
11
|
+
claim: string;
|
|
12
|
+
citations: readonly EvidenceCitation[];
|
|
13
|
+
apiKey: string;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
read?: <T>(run: () => T) => Promise<T>;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
status: "unavailable";
|
|
19
|
+
verdict: "insufficient_evidence";
|
|
20
|
+
needsReview: boolean;
|
|
21
|
+
reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
evidence: {
|
|
24
|
+
path: string;
|
|
25
|
+
from: number;
|
|
26
|
+
lines: number;
|
|
27
|
+
documentHash: string;
|
|
28
|
+
excerptHash: string;
|
|
29
|
+
}[];
|
|
30
|
+
policy: string;
|
|
31
|
+
scope: string;
|
|
32
|
+
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
33
|
+
confidence: number;
|
|
34
|
+
probabilities: {
|
|
35
|
+
supports: number;
|
|
36
|
+
contradicts: number;
|
|
37
|
+
insufficient_evidence: number;
|
|
38
|
+
};
|
|
39
|
+
needsReview: boolean;
|
|
40
|
+
status: "ok";
|
|
41
|
+
}>;
|