@jmtrin/opencode-kevin 0.7.0 → 0.8.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/README.md +198 -9
- package/dist/migrations/009_v08_team.sql +100 -0
- package/dist/plugin/ArtifactWriter.d.ts +28 -0
- package/dist/plugin/ArtifactWriter.js +35 -2
- package/dist/plugin/ArtifactWriter.js.map +1 -1
- package/dist/plugin/ContextInjector.d.ts +9 -0
- package/dist/plugin/ContextInjector.js +14 -3
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/Curator.d.ts +22 -2
- package/dist/plugin/Curator.js +56 -14
- package/dist/plugin/Curator.js.map +1 -1
- package/dist/plugin/InjectionLedger.d.ts +6 -0
- package/dist/plugin/InjectionLedger.js +6 -0
- package/dist/plugin/InjectionLedger.js.map +1 -1
- package/dist/plugin/Materializer.d.ts +4 -5
- package/dist/plugin/Materializer.js +9 -6
- package/dist/plugin/Materializer.js.map +1 -1
- package/dist/plugin/MemoryService.d.ts +34 -5
- package/dist/plugin/MemoryService.js +215 -7
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +33 -3
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/RepoIdentity.d.ts +113 -0
- package/dist/plugin/RepoIdentity.js +266 -0
- package/dist/plugin/RepoIdentity.js.map +1 -0
- package/dist/plugin/Retrospective.js +9 -0
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/SharedLayer.d.ts +159 -0
- package/dist/plugin/SharedLayer.js +463 -0
- package/dist/plugin/SharedLayer.js.map +1 -0
- package/dist/plugin/index.d.ts +30 -1
- package/dist/plugin/index.js +458 -10
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_approve.js +7 -4
- package/dist/plugin/kevin_approve.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +19 -1
- package/dist/plugin/kevin_audit.js +95 -3
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +9 -0
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/okf-export.d.ts +2 -2
- package/dist/plugin/okf-export.js +15 -7
- package/dist/plugin/okf-export.js.map +1 -1
- package/dist/plugin/okf.d.ts +107 -0
- package/dist/plugin/okf.js +304 -0
- package/dist/plugin/okf.js.map +1 -0
- package/dist/plugin/replay-types.d.ts +29 -287
- package/dist/plugin/replay-types.js +145 -53
- package/dist/plugin/replay-types.js.map +1 -1
- package/migrations/009_v08_team.sql +100 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
|
|
|
10
10
|
- **Signal over Noise**: a quality gate that stores weak lessons without injecting them, an injection ledger with honest `precision_rate`, and two-sided confidence.
|
|
11
11
|
- **Glass Box**: honest measurement replaces estimates — three-way injection settlement (`effective` / `ineffective` / `inconclusive`), human feedback that actually moves confidence, a strict dry-run `kevin_trace`, a read-only `kevin_audit`, and a hermetic replay harness.
|
|
12
12
|
- **Pull**: knowledge earns its way into files the model actually reads — `kevin_propose` generates a reviewable diff, a human approves, and **only then** does Kevin write, inside a frozen marker block, preserving your file's CRLF/BOM/formatting byte-for-byte outside it. Plus three distribution channels (AGENTS.md, skills, references) and a push budget gated by a confidence floor.
|
|
13
|
-
- **Audited**: the v0.4.0 bug catalog (`docs/Kevin_v0.4.0_Bugs.md`) is fully closed — 16/16 bugs fixed and regression-tested.
|
|
13
|
+
- **Audited**: the v0.4.0 bug catalog (`docs/Kevin_v0.4.0_Bugs.md`) is fully closed — 16/16 bugs fixed and regression-tested; the v0.8.0 release audit closed 8/8 bugs (repo identity, session coherence through rekey, port normalization in remotes, refusal semantics, read-path `layer`).
|
|
14
14
|
- **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -22,6 +22,7 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
|
|
|
22
22
|
- [Tools](#tools)
|
|
23
23
|
- [How Kevin measures itself](#how-kevin-measures-itself)
|
|
24
24
|
- [Curation & Pull](#curation--pull)
|
|
25
|
+
- [The shared layer (v0.8.0)](#the-shared-layer-v080)
|
|
25
26
|
- [Replay harness](#replay-harness)
|
|
26
27
|
- [Hooks](#hooks)
|
|
27
28
|
- [Configuration](#configuration)
|
|
@@ -50,7 +51,7 @@ For a **single project**, put the same `plugin` array in `./opencode.json` or `.
|
|
|
50
51
|
|
|
51
52
|
### 2. Restart OpenCode
|
|
52
53
|
|
|
53
|
-
Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes
|
|
54
|
+
Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes 21 tools, including `kevin_facts`, `kevin_conflicts`, `kevin_project`, `kevin_share` and `kevin_sync`.
|
|
54
55
|
|
|
55
56
|
Contradictions de-rank memories and surface conflicts. They never delete, stale, archive, or auto-resolve a memory.
|
|
56
57
|
|
|
@@ -138,13 +139,24 @@ Every tool call is observed; every failure becomes a lesson; every lesson is eit
|
|
|
138
139
|
└─────────────────────────┘ promotion, pattern mining (opt-in)
|
|
139
140
|
```
|
|
140
141
|
|
|
141
|
-
At `session.idle` Kevin also settles injection outcomes, retires stale memories,
|
|
142
|
+
At `session.idle` Kevin also settles injection outcomes, retires stale memories, drafts pull proposals for your review when curation is enabled (see [Curation & Pull](#curation--pull)), and syncs the shared OKF file when the shared layer is enabled.
|
|
143
|
+
|
|
144
|
+
In plain words, the whole loop is:
|
|
145
|
+
|
|
146
|
+
1. **Watch.** Every tool call is observed — what ran, what failed, what fixed it.
|
|
147
|
+
2. **Learn.** Failures become short lessons, deduplicated per error fingerprint and throttled so a noisy failure does not spam.
|
|
148
|
+
3. **Push.** At the next prompt, the best lessons are injected as `<kevin-context>` — capped, confidence-gated, and every rejection counted.
|
|
149
|
+
4. **Write.** With your explicit approval, a lesson can also land in `AGENTS.md`, a skill or a reference file — only ever inside the frozen marker block.
|
|
150
|
+
5. **Share.** With the shared layer on (v0.8.0, opt-in), approved lessons can be exported to a committed `.kevin/knowledge.okf` file that every teammate pulls and imports.
|
|
151
|
+
6. **Retire.** Lessons that stop being true — recurrences, staleness, a human `ignore` — stop being injected. Nothing lives forever by default.
|
|
152
|
+
|
|
153
|
+
Since v0.8.0, memories live in one of two layers: **local** (private to this installation, the model above) and **shared** (a committed `.kevin/knowledge.okf` file projected into the local database, immutable, retrievable and injectable like any local memory). See [The shared layer](#the-shared-layer-v080) — the feature is opt-in and off by default.
|
|
142
154
|
|
|
143
155
|
---
|
|
144
156
|
|
|
145
157
|
## Tools
|
|
146
158
|
|
|
147
|
-
Kevin exposes
|
|
159
|
+
Kevin exposes 21 tools callable by the agent.
|
|
148
160
|
|
|
149
161
|
### `kevin_save`
|
|
150
162
|
|
|
@@ -193,13 +205,13 @@ kevin_recall({ query: "auth", limit: 3 })
|
|
|
193
205
|
|
|
194
206
|
### `kevin_status`
|
|
195
207
|
|
|
196
|
-
Global counts and metrics: memory census, the precision block, the six blocked-gate counters, feedback totals, and the v0.6 block (`schema_version`, `curation_enabled`, emission states, `proposals_pending` — omitted on pre-007 databases).
|
|
208
|
+
Global counts and metrics: memory census, the precision block, the six blocked-gate counters, feedback totals, and the v0.6 block (`schema_version`, `curation_enabled`, emission states, `proposals_pending` — omitted on pre-007 databases). On 009 databases (v0.8.0) it also reports the `v08` block: `repo_id` (a 16-hex hash derived from the identity sources below — never a raw remote URL), `identity_source` (`"declared"` from `.kevin/project.json`, `"remote"` from the git origin URL, or `"path"` fallback), `shared_layer_enabled` and the repo's `shared_entries` count. Omitted on pre-009 databases.
|
|
197
209
|
|
|
198
210
|
```
|
|
199
211
|
kevin_status({})
|
|
200
212
|
// → { "memories": 42, "memories_reflector": 12, "memories_agent": 30,
|
|
201
213
|
// "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
|
|
202
|
-
// "retrospectives": 7, "tool_count":
|
|
214
|
+
// "retrospectives": 7, "tool_count": 21,
|
|
203
215
|
// "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
|
|
204
216
|
// "reflections_throttled": 3, "duplicate_suppressions": 2,
|
|
205
217
|
// "tool_calls_deduped": 0, "patterns_mined": 0,
|
|
@@ -214,7 +226,9 @@ kevin_status({})
|
|
|
214
226
|
// "patterns_promoted_new": 2, "recurrence_by_origin": { "reflector": 3, "causal": 1 },
|
|
215
227
|
// "v06": { "schema_version": "007", "curation_enabled": "1",
|
|
216
228
|
// "skill_emission": "off", "reference_emission": "off",
|
|
217
|
-
// "proposals_pending": 2 }
|
|
229
|
+
// "proposals_pending": 2 },
|
|
230
|
+
// "v08": { "repo_id": "2114ad162af50a25", "identity_source": "remote",
|
|
231
|
+
// "shared_layer_enabled": "0", "shared_entries": 3 } }
|
|
218
232
|
```
|
|
219
233
|
|
|
220
234
|
### `kevin_retrospective`
|
|
@@ -340,6 +354,40 @@ kevin_approve({ proposal_id: "...", decision: "approve" }) // or "reject"
|
|
|
340
354
|
|
|
341
355
|
Regenerates the pull-channel bundles under `~/.opencode-kevin/` — `skills/project-knowledge.md` and `refs/<topic>.md` — reporting per-bundle outcome and the emission state (`on` / `off` / `unavailable`). Registration with the host happens at plugin startup; this tool only materializes and reports.
|
|
342
356
|
|
|
357
|
+
### `kevin_project`
|
|
358
|
+
|
|
359
|
+
Repository identity (v0.8.0). `action: "show"` reports the resolved `repoId`, `identity_source`, `projectId`, the memory counts under each scope and `rekey_available`; `action: "init"` writes `.kevin/project.json` pinning the derived id (refused if it already exists); `action: "rekey"` moves the whole corpus to the resolved `repo_id` in one transaction — without `confirm: true` it is a dry run that mutates nothing, and a monorepo collision is rejected unless `force: true`.
|
|
360
|
+
|
|
361
|
+
A **confirmed** rekey updates the running session live — the identity, the memory service and the shared-layer bridge all move to the new `repo_id` immediately, so `kevin_status`, `kevin_audit` and `kevin_share` keep working without a restart, and the OKF file's `#repo` header is repaired so the shared channel keeps working too.
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
kevin_project({ action: "show" })
|
|
365
|
+
// → { "repoId": "2114ad162af50a25", "source": "remote", "evidence": "origin",
|
|
366
|
+
// "projectId": "8f3c2a1b...", "projectScoped": 41, "repoScoped": 0,
|
|
367
|
+
// "rekeyAvailable": true }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### `kevin_share`
|
|
371
|
+
|
|
372
|
+
Promotes curated memories into the shared layer: plans an export to the OKF file and (with `confirm: true`) writes it through the single write funnel. **A strict dry run by default** — with no `confirm` it returns the plan and its diff, writing nothing. With no `memory_ids`, it selects every `layer='local'`, curated memory whose confidence clears the shared floor. Refusals: `not_okf`, `version_ahead`, `repo_mismatch`, `too_many_entries`, `line_too_long`, `below_floor`, `not_curated`, `unknown_entry`, `parse_damaged` — a typo'd or foreign memory id refuses the whole export (`unknown_entry`) instead of silently sharing a subset.
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
kevin_share({ memory_ids: ["0195a3b2-..."], dry_run: false, confirm: true })
|
|
376
|
+
// → { "memory_ids": [...], "entries_added": 1, "outcome": "written",
|
|
377
|
+
// "okf_path": ".kevin/knowledge.okf", "diff": "--- a/...\n+++ b/..." }
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### `kevin_sync`
|
|
381
|
+
|
|
382
|
+
Ingests the OKF file (asserts → shared-layer projections; tombstones → archived) and returns `{ imported, tombstoned, skipped, reason }`. Idempotent: an unchanged file is a no-op. Also runs automatically at `session.idle` while `shared_layer_enabled = "1"`.
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
kevin_sync({})
|
|
386
|
+
// → { "path": ".kevin/knowledge.okf", "fileHash": "3d2f...", "parsed": 1,
|
|
387
|
+
// "folded": 0, "rejected": 0, "imported": 1, "tombstoned": 0,
|
|
388
|
+
// "skipped": false }
|
|
389
|
+
```
|
|
390
|
+
|
|
343
391
|
---
|
|
344
392
|
|
|
345
393
|
## How Kevin measures itself
|
|
@@ -425,6 +473,131 @@ Rejection history is never deleted: it is the evidence base for the roadmap's ki
|
|
|
425
473
|
|
|
426
474
|
---
|
|
427
475
|
|
|
476
|
+
## The shared layer (v0.8.0)
|
|
477
|
+
|
|
478
|
+
### Two layers, one file
|
|
479
|
+
|
|
480
|
+
Every memory lives in one of two layers:
|
|
481
|
+
|
|
482
|
+
| Layer | Storage | Visibility |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| **local** | `kevin.db` → `memories` | private to this installation (the whole v0.1–v0.7 model) |
|
|
485
|
+
| **shared** | `.kevin/knowledge.okf` (a committed file) → `shared_entries` projections | visible to every teammate who pulls the repo and runs Kevin |
|
|
486
|
+
|
|
487
|
+
The OKF file is the *only* distribution vehicle. Kevin **never** commits it, pushes it, or talks to a server — you commit and push it with the rest of your repository. A teammate's `git pull` + `kevin_sync` (or the automatic sync at `session.idle`) turns every `assert` entry into a read-only projection in their `memories` table, retrievable and injectable like any local memory — but immutable: the shared layer is an exact projection of the committed file, so it is never edited, only re-imported or tombstoned. A tombstone in the file archives the projection everywhere.
|
|
488
|
+
|
|
489
|
+
The whole feature is opt-in: with `shared_layer_enabled = "0"` (the default), Kevin never reads or writes the file, and `session.idle` performs no filesystem access at all.
|
|
490
|
+
|
|
491
|
+
### Where the repo identity comes from
|
|
492
|
+
|
|
493
|
+
Every repo has a `repo_id` — a 16-hex hash that scopes both the OKF file (`#repo`) and the shared projections. Kevin resolves it from three sources, in priority order:
|
|
494
|
+
|
|
495
|
+
| Priority | Source | `identity_source` | Typical case |
|
|
496
|
+
|---|---|---|---|
|
|
497
|
+
| 1 | `.kevin/project.json` (`repo_id` pinned) | `declared` | after `kevin_project init` |
|
|
498
|
+
| 2 | the git `origin` remote URL, hashed | `remote` | a repo cloned from a known remote |
|
|
499
|
+
| 3 | the project path | `path` | a local folder without git |
|
|
500
|
+
|
|
501
|
+
Only the **hash** is ever stored, written or reported — the raw remote URL never reaches the database or the OKF file. `kevin_project` is the mirror of this resolution: `show` tells you which source won, `init` pins it, `rekey` moves the whole corpus when the identity changes (say, the repo got a new remote):
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
startup or kevin_project call
|
|
505
|
+
│
|
|
506
|
+
▼
|
|
507
|
+
resolve(): declared ──► remote ──► path (first source that yields)
|
|
508
|
+
│
|
|
509
|
+
▼
|
|
510
|
+
repo_id (16-hex hash — never the raw URL)
|
|
511
|
+
│
|
|
512
|
+
├── kevin_project show → which source won, counts, rekey_available
|
|
513
|
+
├── kevin_project init → pins the id into .kevin/project.json
|
|
514
|
+
└── kevin_project rekey → one transaction moves everything together:
|
|
515
|
+
DB corpus + live session identity
|
|
516
|
+
+ the OKF file's #repo header
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Because `rekey` re-aligns the running session (not just the database), the natural flow "add a remote → rekey → share" works without restarting OpenCode — and the `#repo` header heal keeps `kevin_share`/`kevin_sync` from refusing the file with `repo_mismatch`.
|
|
520
|
+
|
|
521
|
+
### The round trip, step by step
|
|
522
|
+
|
|
523
|
+
```
|
|
524
|
+
you (repo A) teammate (repo B)
|
|
525
|
+
┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐
|
|
526
|
+
│ kevin_share({ memory_ids, │ │ git pull │
|
|
527
|
+
│ confirm: true }) │ │ │ │
|
|
528
|
+
│ │ 1. identity gate: #repo must │ │ ▼ │
|
|
529
|
+
│ │ match your repo_id │ │ kevin_sync (or automatically at │
|
|
530
|
+
│ │ 2. entry_id = hash(type, │ │ session.idle) │
|
|
531
|
+
│ │ statement, scope) │ │ │ │
|
|
532
|
+
│ │ 3. per-line and corpus limits │ │ ├─ new asserts → shared_entries │
|
|
533
|
+
│ ▼ │ │ │ → projections (layer='shared', │
|
|
534
|
+
│ .kevin/knowledge.okf │ │ │ immutable, retrievable) │
|
|
535
|
+
│ │ (the only thing Kevin writes) │ │ ├─ tombstones → projections │
|
|
536
|
+
│ ▼ │ │ │ archived │
|
|
537
|
+
│ git commit + push │ │ └─ unchanged file → no-op │
|
|
538
|
+
└───────────────────────────────────────┘ └───────────────────────────────────────┘
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Kevin never commits, pushes or fetches anything: the git remote is the transport, and `git commit`/`git push` are yours.
|
|
542
|
+
|
|
543
|
+
### The OKF v2 format
|
|
544
|
+
|
|
545
|
+
One annotated example, byte-exact (the three header lines are always first):
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
#okf 2
|
|
549
|
+
#repo 2114ad162af50a25
|
|
550
|
+
#generated-by opencode-kevin/0.8.0
|
|
551
|
+
{"author_hash":null,"created_at":"2026-08-17T03:34:17Z","entry_id":"2d80f0972a4b8c92","evidence":6,"op":"assert","origin":"pattern","recurrence":0,"scope":"project","statement":"Always use the repository pattern for the data layer","supersedes":null,"type":"rule"}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
- `#okf 2` — the format version. A file with a **higher** version is refused (`version_ahead`), never downgraded.
|
|
555
|
+
- `#repo <16-hex>` — the repo identity the file belongs to. A file from another repo is refused (`repo_mismatch`) — the shared layer never crosses repositories.
|
|
556
|
+
- `#generated-by opencode-kevin/<version>` — provenance of the writer.
|
|
557
|
+
- Each following line is one JSON entry. `op: "assert"` declares knowledge; `op: "tombstone"` retires the `entry_id` it carries. `entry_id` is a deterministic hash of `(type, statement, scope)`, so the same statement from any teammate collides to the same entry — that is what makes imports idempotent.
|
|
558
|
+
- Limits, enforced at export *and* import: `MAX_LINE_BYTES = 4096`, `MAX_ENTRIES = 2000`. A corpus beyond the limits is refused, never truncated.
|
|
559
|
+
|
|
560
|
+
### When git reports a conflict in `.kevin/knowledge.okf`
|
|
561
|
+
|
|
562
|
+
A merge conflict in the OKF file is expected — both sides asserted different knowledge. The counter-intuitive answer: **keep both sides, then run `kevin_sync`.** Kevin's import is merge-friendly by design: entries are idempotent (same `entry_id` → no-op) and disjoint entries coexist in the same file.
|
|
563
|
+
|
|
564
|
+
Worked example — your branch asserted "Always use the repository pattern…" and your teammate's branch asserted "Always wrap file writes in a temp-file + rename helper". Git cannot merge two files that both changed, so it stops:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
<<<<<<< HEAD
|
|
568
|
+
#okf 2
|
|
569
|
+
#repo 2114ad162af50a25
|
|
570
|
+
#generated-by opencode-kevin/0.8.0
|
|
571
|
+
{"op":"assert", ... "statement":"Always use the repository pattern..."}
|
|
572
|
+
=======
|
|
573
|
+
#okf 2
|
|
574
|
+
#repo 2114ad162af50a25
|
|
575
|
+
#generated-by opencode-kevin/0.8.0
|
|
576
|
+
{"op":"assert", ... "statement":"Always wrap file writes in a temp-file + rename helper..."}
|
|
577
|
+
>>>>>>> teammate
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
1. Resolve by keeping both entry lines (drop the `<<<<<<<`, `=======`, `>>>>>>>` markers and the duplicated headers, keep the other headers and both JSON lines).
|
|
581
|
+
2. Save the file — it now contains both assertions.
|
|
582
|
+
3. Run `kevin_sync`. Both entries import; both projections become active; the conflict is resolved and the corpus is the union of both sides.
|
|
583
|
+
|
|
584
|
+
Do **not** delete either side "to make it clean" — that deletes knowledge. The one thing to avoid: a file left with the conflict markers still present will refuse to import (`parse_damaged`), because a `<<<<<<<` line is not a valid entry.
|
|
585
|
+
|
|
586
|
+
### Retiring shared knowledge
|
|
587
|
+
|
|
588
|
+
There is no in-product tool that writes a `tombstone` entry for you (a deliberate v0.8.0 scope decision). To retire a shared entry, edit the file by hand: replace the `assert` line with a `tombstone` line carrying the same `entry_id`, commit, and let teammates' `kevin_sync` archive the projection. The import side always honors tombstones — the export side just has no button for them yet.
|
|
589
|
+
|
|
590
|
+
### Non-goals (asked for, deliberately not built)
|
|
591
|
+
|
|
592
|
+
- **No server.** The file, the git remote, and your own discipline are the only transport.
|
|
593
|
+
- **No account.** No signup, no cloud, no telemetry — `author_identity_mode` defaults to `hashed` and no identity is ever transmitted.
|
|
594
|
+
- **No automatic commit.** Kevin writes the file; git is yours.
|
|
595
|
+
- **No cross-repository corpus.** `#repo` scopes the file: exports are refused (`repo_mismatch`) when the file belongs to another repository. Imports, by contrast, are deliberately tolerant — an entry's validity is decided by its own `entry_id`, so a copied file imports as if it belonged to your repo (the tested, documented behaviour).
|
|
596
|
+
- **No undelete.** A tombstone is a statement, not a recovery mechanism — keep your git history if you need to resurrect an entry.
|
|
597
|
+
- **No tombstone tool (yet).** See [Retiring shared knowledge](#retiring-shared-knowledge) — retirement happens by editing the file.
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
428
601
|
## Replay harness
|
|
429
602
|
|
|
430
603
|
`npm run replay` runs every transcript in `tests/replay/fixtures/` through the plugin against an in-memory database with a frozen clock and prints one table row per transcript (memories created, injection outcomes, `precision_rate`, `coverage_rate`, tokens). Record your own session as a JSON array of typed events (`session.created`, `chat.message`, `tool.before`, `tool.after`, `system.transform`, `compacting`, `session.idle`) with ISO-8601 `at` timestamps, drop it into `tests/replay/fixtures/`, and re-run. The `at` timestamps are the only source of time during replay.
|
|
@@ -442,7 +615,7 @@ Kevin subscribes to 6 OpenCode hooks:
|
|
|
442
615
|
| `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (400 tokens by default, configurable) + optional `<kevin-suggestion>` |
|
|
443
616
|
| `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
|
|
444
617
|
| `event` (`session.created`) | Captures current `sessionID` (skill/reference emissions register at plugin startup, not per session) |
|
|
445
|
-
| `event` (`session.idle`) | Settles injection outcomes; generates the retrospective; boosts positive lessons; penalizes recurring failures; promotes causal patterns and mines patterns (opt-in); drafts curation proposals (`curation_enabled`); flushes metrics |
|
|
618
|
+
| `event` (`session.idle`) | Settles injection outcomes; generates the retrospective; boosts positive lessons; penalizes recurring failures; promotes causal patterns and mines patterns (opt-in); drafts curation proposals (`curation_enabled`); syncs the shared OKF file (`shared_layer_enabled`); flushes metrics |
|
|
446
619
|
|
|
447
620
|
**Redaction**: absolute paths (`C:\Users\...`, `/home/...`) → `<path>` and secrets (`API_KEY=`, `Bearer`, `token`) → `<redacted>` before persisting anything. `<private>…</private>` blocks are swept from tool call args and output before persistence and replaced with `<private: redacted N chars>`.
|
|
448
621
|
|
|
@@ -472,7 +645,7 @@ KevinPlugin(input, {
|
|
|
472
645
|
|
|
473
646
|
### Settings
|
|
474
647
|
|
|
475
|
-
Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are TEXT; booleans compare against `"1"`.
|
|
648
|
+
Read/write via `kevin_config({ action: "list" | "set", ... })`. All 23 values are TEXT; booleans compare against `"1"`.
|
|
476
649
|
|
|
477
650
|
| Setting | Default | Effect |
|
|
478
651
|
|---|---|---|
|
|
@@ -490,6 +663,17 @@ Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are T
|
|
|
490
663
|
| `skill_emission_enabled` | `"0"` | Registers the curated skill with the host at startup (v2 hosts only) |
|
|
491
664
|
| `reference_emission_enabled` | `"0"` | Registers `@kevin/<topic>` references at startup (v2 hosts only) |
|
|
492
665
|
| `injection_confidence_floor` | `"0.6"` | Push gate: memories below this confidence are counted and rejected |
|
|
666
|
+
| `repo_truth_enabled` | `"0"` | Opt-in Project Truth: fact scanning + contradiction detection at `session.idle` |
|
|
667
|
+
| `convention_mining_enabled` | `"0"` | Opt-in deterministic convention mining into `AGENTS.md` |
|
|
668
|
+
| `conflict_detection_enabled` | `"0"` | Opt-in contradiction detection between memories and observed facts |
|
|
669
|
+
| `error_lesson_mode` | `"all"` | Which failures produce lessons: `all` / `rules_only` / `patterns_only` |
|
|
670
|
+
| `shared_layer_enabled` | `"0"` | Master switch for the shared layer. When `"0"`, Kevin never reads or writes the OKF file and `session.idle` performs no filesystem access. Compare with `=== "1"` — a truthiness check would turn the feature on for every installation |
|
|
671
|
+
| `okf_path` | `".kevin/knowledge.okf"` | Project-relative path of the committed OKF file (**string** — always truthy, which is exactly why the layer flag above must not be read as a boolean) |
|
|
672
|
+
| `share_requires_approval` | `"1"` | When `"1"`, `kevin_share` writes only with `confirm: true`; un-curated memories are refused with `not_curated` |
|
|
673
|
+
| `author_identity_mode` | `"hashed"` | Author attribution in exports: `"hashed"` or `"none"` (**string** enum) |
|
|
674
|
+
| `shared_confidence_floor` | `"0.7"` | Gate for `kevin_share` selection and export — a **string**, read with `Number.parseFloat` and clamped to `[0, 1]`. Deliberately stricter than `injection_confidence_floor` (`0.6`): sharing is a commitment, injecting is a suggestion. `parseInt` on this setting would yield `0` and share everything |
|
|
675
|
+
|
|
676
|
+
All 23 settings are TEXT values; booleans and flags compare against `"1"`; the three v0.8 **string-valued** settings are `okf_path`, `author_identity_mode` and `shared_confidence_floor`.
|
|
493
677
|
|
|
494
678
|
---
|
|
495
679
|
|
|
@@ -548,6 +732,9 @@ plugin/
|
|
|
548
732
|
kevin_publish.ts # kevin_publish tool (bundle regeneration)
|
|
549
733
|
kevin_audit.ts # Read-only audit + channels/curation blocks
|
|
550
734
|
kevin_why.ts # kevin_why tool: failure→fix traces + related rules
|
|
735
|
+
SharedLayer.ts # v0.8: OKF plan/apply/tombstone + import → shared projections
|
|
736
|
+
RepoIdentity.ts # v0.8: repo_id resolution (declared → remote → path) + rekey
|
|
737
|
+
okf.ts # v0.8: OKF v2 parse/serialize (headers, entry_id, limits)
|
|
551
738
|
okf-export.ts # kevin_export: OKF/markdown export
|
|
552
739
|
okf-import.ts # kevin_import: bundle parser + import
|
|
553
740
|
confidence.ts # Two-sided computeConfidence (evidence + recurrence + feedback)
|
|
@@ -565,6 +752,8 @@ migrations/
|
|
|
565
752
|
005_v04_signal.sql # recurrence_count, fix_args, last_injected_at
|
|
566
753
|
006_v05_glassbox.sql # ignored/archived/superseded_by, feedback, metrics
|
|
567
754
|
007_v06_pull.sql # curation_proposals, artifact_writes, curated/inferable
|
|
755
|
+
008_v07_truth.sql # facts + contradictions, conflict_detection settings
|
|
756
|
+
009_v08_team.sql # shared_entries, okf_imports, the five v0.8 settings
|
|
568
757
|
tests/
|
|
569
758
|
unit/ # component tests
|
|
570
759
|
integration/ # tool-level tests through real components
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin 0.8.0 - Migration 009: Team (additive)
|
|
3
|
+
-- ============================================================
|
|
4
|
+
-- Backward-compatible, additive only. All new columns are
|
|
5
|
+
-- nullable or carry a NOT NULL DEFAULT so legacy rows keep
|
|
6
|
+
-- working without a destructive rebuild.
|
|
7
|
+
--
|
|
8
|
+
-- Scope note: this migration introduces `repo_id`, a SECOND
|
|
9
|
+
-- scoping dimension. `project_id` is retained on every table,
|
|
10
|
+
-- unchanged, as local-path provenance (D8-02). Nothing that
|
|
11
|
+
-- reads `project_id` today stops working.
|
|
12
|
+
-- ============================================================
|
|
13
|
+
|
|
14
|
+
-- 1. shared_entries: the local projection of the committed OKF file.
|
|
15
|
+
-- One row per (repo_id, entry_id). Rewritten by SharedLayer.import(),
|
|
16
|
+
-- never edited by hand, never the source of truth - the file is.
|
|
17
|
+
-- No REFERENCES to memories: an entry may arrive from a teammate
|
|
18
|
+
-- before any local memory corresponds to it (D8-12).
|
|
19
|
+
CREATE TABLE IF NOT EXISTS shared_entries (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
repo_id TEXT NOT NULL,
|
|
22
|
+
entry_id TEXT NOT NULL,
|
|
23
|
+
type TEXT NOT NULL CHECK (type IN ('decision', 'rule', 'pattern', 'solution')),
|
|
24
|
+
statement TEXT NOT NULL,
|
|
25
|
+
scope TEXT,
|
|
26
|
+
confidence REAL NOT NULL DEFAULT 0.0,
|
|
27
|
+
evidence INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
origin TEXT NOT NULL DEFAULT 'shared',
|
|
29
|
+
author_hash TEXT,
|
|
30
|
+
op TEXT NOT NULL CHECK (op IN ('assert', 'tombstone')) DEFAULT 'assert',
|
|
31
|
+
supersedes TEXT,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- 1b. Identity is (repo_id, entry_id). The UNIQUE index is what makes
|
|
37
|
+
-- import() an idempotent upsert instead of an append.
|
|
38
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_shared_entries
|
|
39
|
+
ON shared_entries(repo_id, entry_id);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_shared_entries_op
|
|
41
|
+
ON shared_entries(op);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_shared_entries_type
|
|
43
|
+
ON shared_entries(type);
|
|
44
|
+
|
|
45
|
+
-- 2. okf_imports: append-only audit of every read of the shared file,
|
|
46
|
+
-- including no-op reads and refusals. `file_hash` drives the skip
|
|
47
|
+
-- path in SharedLayer.import() (D8-14).
|
|
48
|
+
CREATE TABLE IF NOT EXISTS okf_imports (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
repo_id TEXT NOT NULL,
|
|
51
|
+
path TEXT NOT NULL,
|
|
52
|
+
file_hash TEXT,
|
|
53
|
+
entries_parsed INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
entries_folded INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
entries_rejected INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_okf_imports_repo
|
|
61
|
+
ON okf_imports(repo_id, imported_at);
|
|
62
|
+
|
|
63
|
+
-- 3. memories: the second scoping dimension and the layer marker.
|
|
64
|
+
-- repo_id is NULLABLE and back-filled by the post-apply hook, not by
|
|
65
|
+
-- a DEFAULT: the value depends on the row's existing project_id and
|
|
66
|
+
-- SQLite cannot express that in a column default.
|
|
67
|
+
-- layer carries NO CHECK constraint - widening it later would force
|
|
68
|
+
-- the migration-004 rebuild path (D8-07). Enforced in TypeScript.
|
|
69
|
+
ALTER TABLE memories ADD COLUMN repo_id TEXT;
|
|
70
|
+
ALTER TABLE memories ADD COLUMN layer TEXT NOT NULL DEFAULT 'local';
|
|
71
|
+
ALTER TABLE memories ADD COLUMN shared_entry_id TEXT;
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_memories_repo_id
|
|
74
|
+
ON memories(repo_id, status);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer
|
|
76
|
+
ON memories(layer);
|
|
77
|
+
|
|
78
|
+
-- 4. kevin_metrics: seed the six v0.8 counters (33 -> 39).
|
|
79
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
|
|
80
|
+
('shared_entries_total', 0),
|
|
81
|
+
('shared_entries_imported', 0),
|
|
82
|
+
('shared_entries_exported', 0),
|
|
83
|
+
('okf_merge_folds', 0),
|
|
84
|
+
('rekey_events', 0),
|
|
85
|
+
('injections_from_shared', 0);
|
|
86
|
+
|
|
87
|
+
-- 5. kevin_settings: seed the five v0.8 flags (18 -> 23).
|
|
88
|
+
-- shared_layer_enabled defaults OFF: this release must be opted into,
|
|
89
|
+
-- because its first side effect is a new file in the user's repository.
|
|
90
|
+
-- shared_confidence_floor (0.7) is deliberately STRICTER than
|
|
91
|
+
-- injection_confidence_floor (0.6) - see 5.7.
|
|
92
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
|
|
93
|
+
('shared_layer_enabled', '0'),
|
|
94
|
+
('okf_path', '.kevin/knowledge.okf'),
|
|
95
|
+
('share_requires_approval', '1'),
|
|
96
|
+
('author_identity_mode', 'hashed'),
|
|
97
|
+
('shared_confidence_floor', '0.7');
|
|
98
|
+
|
|
99
|
+
-- 6. Seed version 009.
|
|
100
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('009');
|
|
@@ -3,6 +3,27 @@ import type { Metrics } from "./metrics.js";
|
|
|
3
3
|
export declare const MARKER_BEGIN = "<!-- kevin:begin \u2014 curated by opencode-kevin, safe to edit -->";
|
|
4
4
|
export declare const MARKER_END = "<!-- kevin:end -->";
|
|
5
5
|
export type WriteOutcome = "written" | "noop" | "refused";
|
|
6
|
+
/**
|
|
7
|
+
* v0.8.0 (K8-019 / D8-08) — the two write modes. `markers` is the
|
|
8
|
+
* v0.6.0 behaviour, byte for byte: a splice between the two marker
|
|
9
|
+
* comments, used for `AGENTS.md`, a file humans edit. `whole` replaces
|
|
10
|
+
* the entire file and is used only for Kevin-owned paths such as
|
|
11
|
+
* `.kevin/knowledge.okf` — a file humans do not hand-edit.
|
|
12
|
+
*/
|
|
13
|
+
export type WriteMode = "markers" | "whole";
|
|
14
|
+
export interface WriteRequest {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly mode: WriteMode;
|
|
17
|
+
/** marker block body ("markers"), or whole-file content ("whole"). */
|
|
18
|
+
readonly content: string;
|
|
19
|
+
/**
|
|
20
|
+
* Caller-side refusal reason (K8-020 / D6-03): when present, the plan
|
|
21
|
+
* is refused — nothing is written, and the refusal is audited with
|
|
22
|
+
* both hashes. The refusal conditions belong to the caller; the
|
|
23
|
+
* writer only records them.
|
|
24
|
+
*/
|
|
25
|
+
readonly refusal?: string;
|
|
26
|
+
}
|
|
6
27
|
export interface WritePlan {
|
|
7
28
|
readonly path: string;
|
|
8
29
|
readonly before: string;
|
|
@@ -44,6 +65,13 @@ export declare class ArtifactWriter {
|
|
|
44
65
|
private readonly metrics;
|
|
45
66
|
constructor(store: Store, projectId: string, metrics?: Metrics | null);
|
|
46
67
|
plan(path: string, body: string): WritePlan;
|
|
68
|
+
plan(request: WriteRequest): WritePlan;
|
|
69
|
+
/**
|
|
70
|
+
* K8-019 (D8-08) — the single write funnel: every file Kevin writes
|
|
71
|
+
* goes through this method, which is the ONLY call site of `apply()`
|
|
72
|
+
* in the plugin (asserted by tests/unit/single_write_path.test.ts).
|
|
73
|
+
*/
|
|
74
|
+
write(request: WriteRequest, proposalId?: string): WriteOutcome;
|
|
47
75
|
apply(plan: WritePlan, proposalId?: string): WriteOutcome;
|
|
48
76
|
private renameTemp;
|
|
49
77
|
private audit;
|
|
@@ -84,7 +84,11 @@ export class ArtifactWriter {
|
|
|
84
84
|
this.projectId = projectId;
|
|
85
85
|
this.metrics = metrics ?? null;
|
|
86
86
|
}
|
|
87
|
-
plan(
|
|
87
|
+
plan(pathOrRequest, body) {
|
|
88
|
+
const request = typeof pathOrRequest === "string"
|
|
89
|
+
? { path: pathOrRequest, mode: "markers", content: body ?? "" }
|
|
90
|
+
: pathOrRequest;
|
|
91
|
+
const { path, mode, content } = request;
|
|
88
92
|
let before;
|
|
89
93
|
try {
|
|
90
94
|
// Read as Buffer, not as utf8 text: readFileSync's text decoding
|
|
@@ -100,10 +104,31 @@ export class ArtifactWriter {
|
|
|
100
104
|
throw err;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
if (mode === "whole") {
|
|
108
|
+
// K8-019 (D8-08) — the whole-file path. The file is Kevin-owned,
|
|
109
|
+
// so there are no markers, no sanitization and no EOL
|
|
110
|
+
// normalization: the rendered bytes are written as-is, which is
|
|
111
|
+
// what makes a re-render of the same content a `noop`. A
|
|
112
|
+
// caller-side refusal leaves the file untouched and is audited
|
|
113
|
+
// like any other refusal: after = before, both hashes recorded.
|
|
114
|
+
const refusal = request.refusal;
|
|
115
|
+
const refused = refusal !== undefined;
|
|
116
|
+
const after = refused ? before : content;
|
|
117
|
+
return {
|
|
118
|
+
path,
|
|
119
|
+
before,
|
|
120
|
+
after,
|
|
121
|
+
diff: unifiedDiff(path, before, after),
|
|
122
|
+
outcome: refused ? "refused" : after === before ? "noop" : "written",
|
|
123
|
+
...(refusal !== undefined ? { reason: refusal } : {}),
|
|
124
|
+
hashBefore: sha256(before),
|
|
125
|
+
hashAfter: sha256(after),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
103
128
|
const eol = detectEol(before);
|
|
104
129
|
// Rule 9 — sanitation happens in plan(), before hashing, so the hashes
|
|
105
130
|
// describe what was actually written.
|
|
106
|
-
const bodyEol = normalizeEol(sanitizeArtifactBody(
|
|
131
|
+
const bodyEol = normalizeEol(sanitizeArtifactBody(content), eol);
|
|
107
132
|
const firstBegin = before.indexOf(MARKER_BEGIN);
|
|
108
133
|
const firstEnd = before.indexOf(MARKER_END);
|
|
109
134
|
let after;
|
|
@@ -176,6 +201,14 @@ export class ArtifactWriter {
|
|
|
176
201
|
hashAfter: sha256(after),
|
|
177
202
|
};
|
|
178
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* K8-019 (D8-08) — the single write funnel: every file Kevin writes
|
|
206
|
+
* goes through this method, which is the ONLY call site of `apply()`
|
|
207
|
+
* in the plugin (asserted by tests/unit/single_write_path.test.ts).
|
|
208
|
+
*/
|
|
209
|
+
write(request, proposalId) {
|
|
210
|
+
return this.apply(this.plan(request), proposalId);
|
|
211
|
+
}
|
|
179
212
|
// v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
|
|
180
213
|
apply(plan, proposalId) {
|
|
181
214
|
// Rule 8 — refusals and noops still leave an audit trail; a refusal
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArtifactWriter.js","sourceRoot":"","sources":["../../plugin/ArtifactWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACN,SAAS,EACT,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,UAAU,EACV,SAAS,GACT,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,6EAA6E;AAC7E,+EAA+E;AAC/E,2BAA2B;AAC3B,MAAM,CAAC,MAAM,YAAY,GACxB,gEAAgE,CAAC;AAClE,MAAM,CAAC,MAAM,UAAU,GAAG,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"ArtifactWriter.js","sourceRoot":"","sources":["../../plugin/ArtifactWriter.ts"],"names":[],"mappings":"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"}
|
|
@@ -195,6 +195,15 @@ export declare class ContextInjector {
|
|
|
195
195
|
/**
|
|
196
196
|
* v0.4.0 (K4-017) — one ledger row per admitted memory (plan §5.2).
|
|
197
197
|
* Token attribution uses the memory's share of the final block.
|
|
198
|
+
*
|
|
199
|
+
* v0.8.0 (K8-024 / plan §5.7) — shared projections are recorded even
|
|
200
|
+
* though they carry NO fingerprint by design (K8-017 — it is a
|
|
201
|
+
* different identity dimension): the memory id is their identity in
|
|
202
|
+
* the ledger, and recording them is what makes `injections_from_shared`
|
|
203
|
+
* observable at all. No tool call can ever match a memory id, so
|
|
204
|
+
* settle() marks the row inconclusive — excluded from the precision
|
|
205
|
+
* denominator (K5-005) — and the BUG-015 skip is unchanged for local
|
|
206
|
+
* notes without a fingerprint.
|
|
198
207
|
*/
|
|
199
208
|
private recordInjections;
|
|
200
209
|
/**
|
|
@@ -434,21 +434,32 @@ Consider adding this convention to AGENTS.md:
|
|
|
434
434
|
/**
|
|
435
435
|
* v0.4.0 (K4-017) — one ledger row per admitted memory (plan §5.2).
|
|
436
436
|
* Token attribution uses the memory's share of the final block.
|
|
437
|
+
*
|
|
438
|
+
* v0.8.0 (K8-024 / plan §5.7) — shared projections are recorded even
|
|
439
|
+
* though they carry NO fingerprint by design (K8-017 — it is a
|
|
440
|
+
* different identity dimension): the memory id is their identity in
|
|
441
|
+
* the ledger, and recording them is what makes `injections_from_shared`
|
|
442
|
+
* observable at all. No tool call can ever match a memory id, so
|
|
443
|
+
* settle() marks the row inconclusive — excluded from the precision
|
|
444
|
+
* denominator (K5-005) — and the BUG-015 skip is unchanged for local
|
|
445
|
+
* notes without a fingerprint.
|
|
437
446
|
*/
|
|
438
447
|
recordInjections(admitted, sessionId, tag, block) {
|
|
439
448
|
if (this.ledger === null)
|
|
440
449
|
return;
|
|
441
450
|
const blockTokens = estimateTokens(block);
|
|
442
|
-
const measurable = admitted.filter((m) => m.fingerprint);
|
|
443
451
|
const perMemory = Math.max(1, Math.round(blockTokens / admitted.length));
|
|
444
452
|
const hook = tag === "context" ? "pre_prompt" : "compacting";
|
|
445
|
-
for (const m of
|
|
453
|
+
for (const m of admitted) {
|
|
454
|
+
if (!m.fingerprint && m.layer !== "shared")
|
|
455
|
+
continue;
|
|
446
456
|
this.ledger.record({
|
|
447
457
|
memoryId: m.id,
|
|
448
|
-
fingerprint: m.fingerprint,
|
|
458
|
+
fingerprint: m.fingerprint ?? m.id,
|
|
449
459
|
sessionId,
|
|
450
460
|
hook,
|
|
451
461
|
tokens: perMemory,
|
|
462
|
+
layer: m.layer ?? "local",
|
|
452
463
|
});
|
|
453
464
|
}
|
|
454
465
|
}
|