@jmtrin/kevin-core 2.0.0 → 2.1.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/dist/RepoTruth.js +6 -1
- package/dist/Retrospective.d.ts +1 -1
- package/dist/Retrospective.js +2 -0
- package/dist/contract.js +13 -4
- package/dist/import-host.d.ts +1 -1
- package/dist/import-host.js +33 -16
- package/dist/index.d.ts +11 -10
- package/dist/index.js +7 -5
- package/dist/metrics.d.ts +1 -1
- package/dist/metrics.js +2 -0
- package/dist/migrations/015_v21_relay.sql +20 -0
- package/dist/okf-shards.d.ts +1 -1
- package/dist/okf-shards.js +9 -4
- package/dist/okf.js +2 -3
- package/dist/sources/ClaudeMemorySource.js +39 -13
- package/dist/sources/CodexMemoriesSource.js +15 -4
- package/dist/sources/IdleSync.d.ts +3 -0
- package/dist/sources/IdleSync.js +155 -7
- package/dist/sources/OpencodeNativeSource.d.ts +5 -0
- package/dist/sources/OpencodeNativeSource.js +117 -15
- package/dist/sources/OpencodePluginSource.js +3 -1
- package/dist/sources/deletion.d.ts +18 -0
- package/dist/sources/deletion.js +54 -0
- package/package.json +1 -1
package/dist/RepoTruth.js
CHANGED
|
@@ -393,7 +393,12 @@ export class RepoTruth {
|
|
|
393
393
|
for (const file of ["package.json", "tsconfig.json"]) {
|
|
394
394
|
try {
|
|
395
395
|
const st = statSync(join(this.projectRoot, file));
|
|
396
|
-
|
|
396
|
+
// Include size alongside mtime so a same-tick rewrite with
|
|
397
|
+
// different bytes is still detected on Windows where mtime
|
|
398
|
+
// granularity is ~15ms. The stored string is compared as an
|
|
399
|
+
// opaque token, so legacy rows (bare mtime) will mismatch once
|
|
400
|
+
// and trigger a re-parse.
|
|
401
|
+
out[file] = `${String(st.mtimeMs)}:${String(st.size)}`;
|
|
397
402
|
}
|
|
398
403
|
catch {
|
|
399
404
|
out[file] = null;
|
package/dist/Retrospective.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type KevinEnv } from "./env.js";
|
|
2
1
|
import type { MemoryService } from "./MemoryService.js";
|
|
3
2
|
import type { Store } from "./Store.js";
|
|
3
|
+
import { type KevinEnv } from "./env.js";
|
|
4
4
|
import type { Metrics } from "./metrics.js";
|
|
5
5
|
export interface RetrospectiveOptions {
|
|
6
6
|
dir?: string;
|
package/dist/Retrospective.js
CHANGED
|
@@ -95,6 +95,8 @@ export const METRIC_KEY_LABELS = {
|
|
|
95
95
|
source_syncs_total: "Sincronizaciones de fuentes (total)",
|
|
96
96
|
source_dedup_skips_total: "Skips por dedup de fuentes (total)",
|
|
97
97
|
okf_v3_files_written: "Archivos OKF v3 escritos",
|
|
98
|
+
// v2.1.0 (K21-005) — Relay deletion metric
|
|
99
|
+
source_deletions_total: "Borrados de fuente sincronizados (total)",
|
|
98
100
|
};
|
|
99
101
|
function originLabel(origin) {
|
|
100
102
|
if (origin === "reflector")
|
package/dist/contract.js
CHANGED
|
@@ -3,8 +3,8 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { MARKER_BEGIN, MARKER_END } from "./ArtifactWriter.js";
|
|
6
|
-
import { resolveEnv } from "./env.js";
|
|
7
6
|
import { METRIC_KEY_LABELS } from "./Retrospective.js";
|
|
7
|
+
import { resolveEnv } from "./env.js";
|
|
8
8
|
import { fnv1a64 } from "./fingerprint.js";
|
|
9
9
|
import { KEVIN_CONFIG_KEYS } from "./index.js";
|
|
10
10
|
import { MAX_ENTRIES, MAX_LINE_BYTES } from "./okf.js";
|
|
@@ -122,7 +122,12 @@ export const CONTRACT_SKILLS_LAYOUT = {
|
|
|
122
122
|
canonical_dir: ".agents/skills",
|
|
123
123
|
skill_dir: "kevin-knowledge",
|
|
124
124
|
files: ["SKILL.md", "references/*.md"],
|
|
125
|
-
frontmatter_fields: [
|
|
125
|
+
frontmatter_fields: [
|
|
126
|
+
"name",
|
|
127
|
+
"description",
|
|
128
|
+
"metadata.generator",
|
|
129
|
+
"metadata.repo_id",
|
|
130
|
+
],
|
|
126
131
|
mirror_policy: "copy (not symlink) to .claude/skills and .cursor/skills when enabled",
|
|
127
132
|
};
|
|
128
133
|
// v2.0.0 (K16-001) — C-13 MIF profile, since 1.5.0
|
|
@@ -220,6 +225,7 @@ export const CONTRACT_METRIC_ADDITIONS = [
|
|
|
220
225
|
{ name: "source_syncs_total", since: "2.0.0" },
|
|
221
226
|
{ name: "source_dedup_skips_total", since: "2.0.0" },
|
|
222
227
|
{ name: "okf_v3_files_written", since: "2.0.0" },
|
|
228
|
+
{ name: "source_deletions_total", since: "2.1.0" },
|
|
223
229
|
];
|
|
224
230
|
/**
|
|
225
231
|
* v1.4.0 (K14-006 / plan §4.3) — config keys added after the freeze,
|
|
@@ -240,6 +246,7 @@ export const CONTRACT_CONFIG_ADDITIONS = [
|
|
|
240
246
|
{ name: "source_codex_memories", since: "2.0.0" },
|
|
241
247
|
{ name: "source_opencode_native", since: "2.0.0" },
|
|
242
248
|
{ name: "sources_enabled", since: "2.0.0" },
|
|
249
|
+
{ name: "source_deletion_sync", since: "2.1.0" },
|
|
243
250
|
];
|
|
244
251
|
/**
|
|
245
252
|
* v2.0.0 (K16-004 / plan §5.1) — removed settings (retirements)
|
|
@@ -378,7 +385,7 @@ export function describeContract(_input) {
|
|
|
378
385
|
stability: "forward-only",
|
|
379
386
|
since: "0.1.0",
|
|
380
387
|
value: {
|
|
381
|
-
schema_version: "
|
|
388
|
+
schema_version: "015",
|
|
382
389
|
migrations_forward_only: true,
|
|
383
390
|
},
|
|
384
391
|
},
|
|
@@ -417,7 +424,9 @@ export function describeContract(_input) {
|
|
|
417
424
|
title: "Core public exports",
|
|
418
425
|
stability: "frozen",
|
|
419
426
|
since: "1.3.0",
|
|
420
|
-
value: {
|
|
427
|
+
value: {
|
|
428
|
+
exports: [...CONTRACT_CORE_EXPORTS].sort((a, b) => a.name.localeCompare(b.name)),
|
|
429
|
+
},
|
|
421
430
|
},
|
|
422
431
|
{
|
|
423
432
|
id: "C-11",
|
package/dist/import-host.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type KevinEnv } from "./env.js";
|
|
2
1
|
import type { MemoryService } from "./MemoryService.js";
|
|
3
2
|
import type { Store } from "./Store.js";
|
|
3
|
+
import { type KevinEnv } from "./env.js";
|
|
4
4
|
import type { Metrics } from "./metrics.js";
|
|
5
5
|
export interface HostImportReport {
|
|
6
6
|
files_scanned: number;
|
package/dist/import-host.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// K15-011/012/013 — Host importers (plan §4.5)
|
|
2
2
|
// Defensive markdown parsers, no YAML lib, same naive parser as validator.
|
|
3
|
-
import { existsSync, lstatSync,
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { QualityGate } from "./QualityGate.js";
|
|
5
6
|
import { resolveEnv } from "./env.js";
|
|
6
7
|
import { fingerprint as computeFingerprint } from "./fingerprint.js";
|
|
7
|
-
import { QualityGate } from "./QualityGate.js";
|
|
8
8
|
const MAX_FILE_BYTES = 1 * 1024 * 1024;
|
|
9
9
|
const MAX_CANDIDATES = 5000;
|
|
10
10
|
const CLAUDE_TYPE_MAP = {
|
|
@@ -134,7 +134,9 @@ export function parseClaudeMemory(dataRoot, env) {
|
|
|
134
134
|
}
|
|
135
135
|
// if frontmatter exists but type missing, map to context
|
|
136
136
|
}
|
|
137
|
-
const mapped = rawType
|
|
137
|
+
const mapped = rawType
|
|
138
|
+
? (CLAUDE_TYPE_MAP[rawType.toLowerCase()] ?? "context")
|
|
139
|
+
: "context";
|
|
138
140
|
// bullets
|
|
139
141
|
const bullets = extractBullets(content);
|
|
140
142
|
if (bullets.length === 0) {
|
|
@@ -201,18 +203,31 @@ export function parseCodexMemories(dataRoot, env) {
|
|
|
201
203
|
}
|
|
202
204
|
export function importHostMemories(opts) {
|
|
203
205
|
const dataRoot = opts.dataRoot ?? resolveEnv(opts.env).dataRoot;
|
|
204
|
-
// gate
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
// gate — v2.0.0 retired import_host_memory (K16-005); check sources framework.
|
|
207
|
+
// Legacy fallback kept for pre-014 DBs that still carry the key before migration 014 deletes it.
|
|
208
|
+
const legacy = opts.memoryService.getSetting("import_host_memory", "0");
|
|
209
|
+
if (legacy === "1") {
|
|
210
|
+
// translated by 014; allow but hint new path
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const master = opts.memoryService.getSetting("sources_enabled", "0") === "1";
|
|
214
|
+
const perKey = opts.source === "claude-memory"
|
|
215
|
+
? "source_claude_memory"
|
|
216
|
+
: "source_codex_memories";
|
|
217
|
+
const per = opts.memoryService.getSetting(perKey, "0") === "1";
|
|
218
|
+
if (!master || !per) {
|
|
219
|
+
return {
|
|
220
|
+
files_scanned: 0,
|
|
221
|
+
candidates: 0,
|
|
222
|
+
saved: 0,
|
|
223
|
+
duplicates: 0,
|
|
224
|
+
skipped_weak: 0,
|
|
225
|
+
error: "disabled",
|
|
226
|
+
hint: opts.source === "claude-memory"
|
|
227
|
+
? "Enable with kevin_config set sources_enabled 1 and kevin_config set source_claude_memory 1"
|
|
228
|
+
: "Enable with kevin_config set sources_enabled 1 and kevin_config set source_codex_memories 1",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
216
231
|
}
|
|
217
232
|
let parsed;
|
|
218
233
|
if (opts.source === "claude-memory") {
|
|
@@ -233,7 +248,9 @@ export function importHostMemories(opts) {
|
|
|
233
248
|
// dedup intra-run via fingerprint set
|
|
234
249
|
const seenFingerprints = new Set();
|
|
235
250
|
// existing fingerprints from DB
|
|
236
|
-
const existingRows = opts.store
|
|
251
|
+
const existingRows = opts.store
|
|
252
|
+
.prepare("SELECT fingerprint FROM memories WHERE fingerprint IS NOT NULL")
|
|
253
|
+
.all();
|
|
237
254
|
for (const r of existingRows)
|
|
238
255
|
seenFingerprints.add(r.fingerprint);
|
|
239
256
|
for (const c of candidates) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./Store.js";
|
|
2
2
|
export * from "./Migrate.js";
|
|
3
|
-
export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn } from "./MemoryService.js";
|
|
4
|
-
export type { Memory, SlimMemory, SlimMemoryWithEvidence, SaveInput, QueryInput, GetRelevantInput, MemoryType, MemoryScope, MemoryOrigin, MemoryUpdateResult } from "./MemoryService.js";
|
|
3
|
+
export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn, } from "./MemoryService.js";
|
|
4
|
+
export type { Memory, SlimMemory, SlimMemoryWithEvidence, SaveInput, QueryInput, GetRelevantInput, MemoryType, MemoryScope, MemoryOrigin, MemoryUpdateResult, } from "./MemoryService.js";
|
|
5
5
|
export * from "./ToolCallObserver.js";
|
|
6
6
|
export * from "./Reflector.js";
|
|
7
7
|
export * from "./ContextInjector.js";
|
|
@@ -38,23 +38,23 @@ export * from "./memory-format.js";
|
|
|
38
38
|
export * from "./redact.js";
|
|
39
39
|
export * from "./uuid.js";
|
|
40
40
|
export * from "./sqlite-adapter.js";
|
|
41
|
-
export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable } from "./columns.js";
|
|
41
|
+
export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable, } from "./columns.js";
|
|
42
42
|
export * from "./time-ms.js";
|
|
43
43
|
export * from "./idle-pipeline.js";
|
|
44
44
|
export * from "./replay.js";
|
|
45
45
|
export * from "./replay-types.js";
|
|
46
46
|
export * from "./ChatBridge.js";
|
|
47
47
|
export * from "./DashboardHtml.js";
|
|
48
|
-
export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox } from "./TuiActions.js";
|
|
49
|
-
export type { MailboxReadResult, PendingProposal, ProcessDeps, ActionStatus } from "./TuiActions.js";
|
|
48
|
+
export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox, } from "./TuiActions.js";
|
|
49
|
+
export type { MailboxReadResult, PendingProposal, ProcessDeps, ActionStatus, } from "./TuiActions.js";
|
|
50
50
|
export * from "./TuiSnapshots.js";
|
|
51
|
-
export type { ProposalView, ConflictView, HealthView, TuiSnapshotSet, TuiAction, ActionResult } from "./tui-types.js";
|
|
51
|
+
export type { ProposalView, ConflictView, HealthView, TuiSnapshotSet, TuiAction, ActionResult, } from "./tui-types.js";
|
|
52
52
|
export * from "./capabilities.js";
|
|
53
53
|
export * from "./host.js";
|
|
54
54
|
export { V2_SPECIFIER } from "./native.js";
|
|
55
|
-
export type { NativeDeps, NativeRegistration, SettingsReader } from "./native.js";
|
|
55
|
+
export type { NativeDeps, NativeRegistration, SettingsReader, } from "./native.js";
|
|
56
56
|
export * from "./kevin_approve.js";
|
|
57
|
-
export { buildAudit, type AuditReport, type ChannelReport, type CurationReport, type TruthReport, type EmissionState } from "./kevin_audit.js";
|
|
57
|
+
export { buildAudit, type AuditReport, type ChannelReport, type CurationReport, type TruthReport, type EmissionState, } from "./kevin_audit.js";
|
|
58
58
|
export * from "./kevin_bench.js";
|
|
59
59
|
export * from "./kevin_conflicts.js";
|
|
60
60
|
export * from "./kevin_contract.js";
|
|
@@ -73,7 +73,8 @@ export * from "./import-host.js";
|
|
|
73
73
|
export * from "./okf-shards.js";
|
|
74
74
|
export * from "./sources/MemorySource.js";
|
|
75
75
|
export * from "./sources/IdleSync.js";
|
|
76
|
+
export * from "./sources/deletion.js";
|
|
76
77
|
export * from "./env.js";
|
|
77
|
-
export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version"];
|
|
78
|
+
export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version", "source_deletion_sync"];
|
|
78
79
|
export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
|
|
79
|
-
export declare const KEVIN_VERSION = "2.
|
|
80
|
+
export declare const KEVIN_VERSION = "2.1.0";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export * from "./Store.js";
|
|
4
4
|
export * from "./Migrate.js";
|
|
5
5
|
// MemoryService has duplicate hasRepoIdColumn with columns.ts — export selectively (keep MemoryService's hasRepoIdColumn, columns provides other helpers)
|
|
6
|
-
export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn } from "./MemoryService.js";
|
|
6
|
+
export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn, } from "./MemoryService.js";
|
|
7
7
|
export * from "./ToolCallObserver.js";
|
|
8
8
|
export * from "./Reflector.js";
|
|
9
9
|
export * from "./ContextInjector.js";
|
|
@@ -41,7 +41,7 @@ export * from "./redact.js";
|
|
|
41
41
|
export * from "./uuid.js";
|
|
42
42
|
export * from "./sqlite-adapter.js";
|
|
43
43
|
// columns has duplicate hasRepoIdColumn with MemoryService — export without that function (MemoryService's version is canonical via index)
|
|
44
|
-
export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable } from "./columns.js";
|
|
44
|
+
export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable, } from "./columns.js";
|
|
45
45
|
export * from "./time-ms.js";
|
|
46
46
|
export * from "./idle-pipeline.js";
|
|
47
47
|
export * from "./replay.js";
|
|
@@ -49,7 +49,7 @@ export * from "./replay-types.js";
|
|
|
49
49
|
export * from "./ChatBridge.js";
|
|
50
50
|
export * from "./DashboardHtml.js";
|
|
51
51
|
// TuiActions duplicates proposalToken with DashboardHtml — export selectively (exclude proposalToken)
|
|
52
|
-
export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox } from "./TuiActions.js";
|
|
52
|
+
export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox, } from "./TuiActions.js";
|
|
53
53
|
export * from "./TuiSnapshots.js";
|
|
54
54
|
// shims for isolation
|
|
55
55
|
export * from "./capabilities.js";
|
|
@@ -57,7 +57,7 @@ export * from "./host.js";
|
|
|
57
57
|
export { V2_SPECIFIER } from "./native.js";
|
|
58
58
|
// kevin_* handlers — avoid duplicate EmissionState / NativeDeps
|
|
59
59
|
export * from "./kevin_approve.js";
|
|
60
|
-
export { buildAudit } from "./kevin_audit.js";
|
|
60
|
+
export { buildAudit, } from "./kevin_audit.js";
|
|
61
61
|
export * from "./kevin_bench.js";
|
|
62
62
|
export * from "./kevin_conflicts.js";
|
|
63
63
|
export * from "./kevin_contract.js";
|
|
@@ -75,6 +75,7 @@ export * from "./import-host.js";
|
|
|
75
75
|
export * from "./okf-shards.js";
|
|
76
76
|
export * from "./sources/MemorySource.js";
|
|
77
77
|
export * from "./sources/IdleSync.js";
|
|
78
|
+
export * from "./sources/deletion.js";
|
|
78
79
|
// KEVIN_CONFIG_KEYS and related constants — moved from adapter index so core owns the source of truth (C-04).
|
|
79
80
|
// Duplicated here to allow adapter to import from core; adapter will re-export them.
|
|
80
81
|
export * from "./env.js";
|
|
@@ -122,6 +123,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
122
123
|
"source_codex_memories",
|
|
123
124
|
"source_opencode_native",
|
|
124
125
|
"okf_write_version",
|
|
126
|
+
"source_deletion_sync",
|
|
125
127
|
];
|
|
126
128
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
127
|
-
export const KEVIN_VERSION = "2.
|
|
129
|
+
export const KEVIN_VERSION = "2.1.0";
|
package/dist/metrics.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Store } from "./Store.js";
|
|
|
6
6
|
* underlying table is empty (e.g., before 003 is applied, on a fresh
|
|
7
7
|
* :memory: test DB, or after a manual wipe).
|
|
8
8
|
*/
|
|
9
|
-
export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "hook_fires_total", "hook_errors_total", "hooks_dead_total", "injections_suppressed_dead_hook", "native_registrations_total", "native_registration_failures", "perf_samples_recorded", "perf_budget_breaches", "dispose_fires_total", "dispose_misses_total", "contract_digest_changes", "bench_runs_total", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total", "skills_emitted_total", "mif_exports_total", "mif_imports_total", "source_syncs_total", "source_dedup_skips_total", "okf_v3_files_written"];
|
|
9
|
+
export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "hook_fires_total", "hook_errors_total", "hooks_dead_total", "injections_suppressed_dead_hook", "native_registrations_total", "native_registration_failures", "perf_samples_recorded", "perf_budget_breaches", "dispose_fires_total", "dispose_misses_total", "contract_digest_changes", "bench_runs_total", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total", "skills_emitted_total", "mif_exports_total", "mif_imports_total", "source_syncs_total", "source_dedup_skips_total", "okf_v3_files_written", "source_deletions_total"];
|
|
10
10
|
export type MetricKey = (typeof METRIC_KEYS)[number];
|
|
11
11
|
/**
|
|
12
12
|
* Cheap token estimate used when bumping the `tokens_injected_*` counters.
|
package/dist/metrics.js
CHANGED
|
@@ -93,6 +93,8 @@ export const METRIC_KEYS = [
|
|
|
93
93
|
"source_syncs_total",
|
|
94
94
|
"source_dedup_skips_total",
|
|
95
95
|
"okf_v3_files_written",
|
|
96
|
+
// v2.1.0 (K21-005 / plan §4.3) — Relay deletion metric (opt-in, default 0)
|
|
97
|
+
"source_deletions_total",
|
|
96
98
|
];
|
|
97
99
|
const DEFAULT_FLUSH_MS = 1000;
|
|
98
100
|
function zeroCache() {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin v2.1.0 "Relay" — Source deletion + Relay metrics
|
|
3
|
+
-- Migration 015. Forward-only. Additive only.
|
|
4
|
+
-- ============================================================
|
|
5
|
+
|
|
6
|
+
-- 1. Add source provenance column to memories (K21-005)
|
|
7
|
+
-- Stores the MemorySource name (opencode-plugin, claude-memory, codex-memories, opencode-native)
|
|
8
|
+
-- Nullable for legacy rows; new source-inserted rows populate it.
|
|
9
|
+
ALTER TABLE memories ADD COLUMN source TEXT;
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source) WHERE source IS NOT NULL;
|
|
12
|
+
|
|
13
|
+
-- 2. New metric: source_deletions_total (K21-005)
|
|
14
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('source_deletions_total', 0);
|
|
15
|
+
|
|
16
|
+
-- 3. New settings seed (K21-005, D21-03: opt-in default 0)
|
|
17
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('source_deletion_sync', '0');
|
|
18
|
+
|
|
19
|
+
-- 4. Version marker
|
|
20
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('015');
|
package/dist/okf-shards.d.ts
CHANGED
package/dist/okf-shards.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// K16-008 — Shard reader/writer (minimal stub, satisfies typecheck and tests for 1999/2000/2001/4500)
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { MAX_ENTRIES, parse, serialize } from "./okf.js";
|
|
5
5
|
export const SHARD_CAP = MAX_ENTRIES; // 2000
|
|
@@ -15,7 +15,11 @@ export function readShards(dir) {
|
|
|
15
15
|
if (existsSync(primaryPath))
|
|
16
16
|
files.push(primaryPath);
|
|
17
17
|
// lexicographic shards excluding primary
|
|
18
|
-
const all = existsSync(dir)
|
|
18
|
+
const all = existsSync(dir)
|
|
19
|
+
? readdirSync(dir)
|
|
20
|
+
.filter((f) => f.startsWith("knowledge-") && f.endsWith(".okf"))
|
|
21
|
+
.sort()
|
|
22
|
+
: [];
|
|
19
23
|
for (const f of all) {
|
|
20
24
|
const p = join(dir, f);
|
|
21
25
|
if (!files.includes(p))
|
|
@@ -42,6 +46,7 @@ export function readShards(dir) {
|
|
|
42
46
|
return { entries, files, rejected };
|
|
43
47
|
}
|
|
44
48
|
export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
45
50
|
// idempotent: pack primary to SHARD_CAP, overflow to shards, collapse sparse gaps, delete empty trailing
|
|
46
51
|
const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
|
|
47
52
|
if (okfVersion === 2) {
|
|
@@ -50,7 +55,7 @@ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
|
|
|
50
55
|
writeFileSync(join(dir, PRIMARY), txt, "utf8");
|
|
51
56
|
// delete any stray shards
|
|
52
57
|
if (existsSync(dir)) {
|
|
53
|
-
for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
|
|
58
|
+
for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
|
|
54
59
|
try {
|
|
55
60
|
unlinkSync(join(dir, f));
|
|
56
61
|
}
|
|
@@ -86,7 +91,7 @@ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
|
|
|
86
91
|
}
|
|
87
92
|
// delete any shards beyond kept (sparse gaps)
|
|
88
93
|
if (existsSync(dir)) {
|
|
89
|
-
for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
|
|
94
|
+
for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
|
|
90
95
|
const p = join(dir, f);
|
|
91
96
|
if (!toKeep.includes(p) && existsSync(p))
|
|
92
97
|
try {
|
package/dist/okf.js
CHANGED
|
@@ -175,9 +175,8 @@ export function parse(text) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
if (version !== OKF_VERSION && version !== OKF_V3) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// For K16-007, only 2 and 3 are valid
|
|
178
|
+
reject(1, "not_okf");
|
|
179
|
+
return { version, repoId: null, entries: [], rejected, folded: 0 };
|
|
181
180
|
}
|
|
182
181
|
if (lines[1]?.startsWith("#repo ")) {
|
|
183
182
|
repoId = lines[1].slice(6) || null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// K16-014 — ClaudeMemorySource (read-only mirror of ~/.claude/memory)
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
export class ClaudeMemorySource {
|
|
6
6
|
enabledFlag;
|
|
7
7
|
root;
|
|
@@ -11,26 +11,52 @@ export class ClaudeMemorySource {
|
|
|
11
11
|
this.enabledFlag = enabledFlag;
|
|
12
12
|
this.root = root;
|
|
13
13
|
}
|
|
14
|
-
enabled() {
|
|
14
|
+
enabled() {
|
|
15
|
+
return this.enabledFlag();
|
|
16
|
+
}
|
|
15
17
|
async fetch() {
|
|
16
18
|
if (!this.enabled())
|
|
17
19
|
return [];
|
|
18
20
|
const memPath = join(this.root, "memory");
|
|
19
|
-
// Could be directory of markdown files or single file — best-effort
|
|
20
21
|
if (!existsSync(memPath))
|
|
21
22
|
return [];
|
|
23
|
+
const out = [];
|
|
22
24
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const st = statSync(memPath);
|
|
26
|
+
if (st.isDirectory()) {
|
|
27
|
+
for (const f of readdirSync(memPath)) {
|
|
28
|
+
if (!f.endsWith(".md") && !f.endsWith(".txt"))
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const txt = readFileSync(join(memPath, f), "utf8");
|
|
32
|
+
for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
|
|
33
|
+
if (out.length >= 100)
|
|
34
|
+
break;
|
|
35
|
+
out.push({ statement: line, type: "rule", scope: null, source: this.name });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
if (st.isFile()) {
|
|
43
|
+
const txt = readFileSync(memPath, "utf8");
|
|
44
|
+
return txt
|
|
45
|
+
.split("\n")
|
|
46
|
+
.map((s) => s.trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.slice(0, 100)
|
|
49
|
+
.map((statement) => ({
|
|
50
|
+
statement,
|
|
51
|
+
type: "rule",
|
|
52
|
+
scope: null,
|
|
53
|
+
source: this.name,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
31
56
|
}
|
|
32
57
|
catch {
|
|
33
|
-
return
|
|
58
|
+
return out;
|
|
34
59
|
}
|
|
60
|
+
return out;
|
|
35
61
|
}
|
|
36
62
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// K16-015 — CodexMemoriesSource (mirror of ~/.codex/memories)
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
export class CodexMemoriesSource {
|
|
6
6
|
enabledFlag;
|
|
7
7
|
root;
|
|
@@ -11,7 +11,9 @@ export class CodexMemoriesSource {
|
|
|
11
11
|
this.enabledFlag = enabledFlag;
|
|
12
12
|
this.root = root;
|
|
13
13
|
}
|
|
14
|
-
enabled() {
|
|
14
|
+
enabled() {
|
|
15
|
+
return this.enabledFlag();
|
|
16
|
+
}
|
|
15
17
|
async fetch() {
|
|
16
18
|
if (!this.enabled())
|
|
17
19
|
return [];
|
|
@@ -24,8 +26,17 @@ export class CodexMemoriesSource {
|
|
|
24
26
|
continue;
|
|
25
27
|
try {
|
|
26
28
|
const txt = readFileSync(join(this.root, f), "utf8");
|
|
27
|
-
for (const line of txt
|
|
28
|
-
|
|
29
|
+
for (const line of txt
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((s) => s.trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.slice(0, 50)) {
|
|
34
|
+
out.push({
|
|
35
|
+
statement: line,
|
|
36
|
+
type: "rule",
|
|
37
|
+
scope: null,
|
|
38
|
+
source: this.name,
|
|
39
|
+
});
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
catch { }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SharedLayer } from "../SharedLayer.js";
|
|
1
2
|
import type { Store } from "../Store.js";
|
|
2
3
|
import type { MemorySource, SourceSyncResult } from "./MemorySource.js";
|
|
3
4
|
export interface SyncDeps {
|
|
@@ -6,5 +7,7 @@ export interface SyncDeps {
|
|
|
6
7
|
metrics?: {
|
|
7
8
|
incr(key: string, by?: number): void;
|
|
8
9
|
};
|
|
10
|
+
sharedLayer?: SharedLayer;
|
|
11
|
+
okfPath?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare function idleSync(deps: SyncDeps): Promise<SourceSyncResult[]>;
|
package/dist/sources/IdleSync.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { fingerprint } from "../fingerprint.js";
|
|
2
|
+
import { collectDeletions } from "./deletion.js";
|
|
2
3
|
function dedupKey(e) {
|
|
3
4
|
// lower precedence wins attribution: fingerprint over normalized statement + scope
|
|
4
5
|
return fingerprint(`${e.type}\0${e.statement}\0${e.scope ?? ""}`);
|
|
5
6
|
}
|
|
7
|
+
function isDeletionSyncEnabled(store) {
|
|
8
|
+
try {
|
|
9
|
+
const row = store
|
|
10
|
+
.prepare("SELECT value FROM kevin_settings WHERE key = 'source_deletion_sync'")
|
|
11
|
+
.get();
|
|
12
|
+
return row?.value === "1";
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
export async function idleSync(deps) {
|
|
7
19
|
const results = [];
|
|
8
20
|
const seen = new Set();
|
|
9
21
|
// Pre-populate dedup with existing memories fingerprints (lowest precedence already wins)
|
|
10
22
|
try {
|
|
11
|
-
const rows = deps.store
|
|
23
|
+
const rows = deps.store
|
|
24
|
+
.prepare("SELECT fingerprint FROM memories")
|
|
25
|
+
.all();
|
|
12
26
|
for (const r of rows)
|
|
13
27
|
if (r.fingerprint)
|
|
14
28
|
seen.add(r.fingerprint);
|
|
@@ -16,10 +30,27 @@ export async function idleSync(deps) {
|
|
|
16
30
|
catch { }
|
|
17
31
|
for (const src of deps.sources.sort((a, b) => a.precedence - b.precedence)) {
|
|
18
32
|
if (!src.enabled()) {
|
|
19
|
-
results.push({
|
|
33
|
+
results.push({
|
|
34
|
+
source: src.name,
|
|
35
|
+
fetched: 0,
|
|
36
|
+
dedupSkipped: 0,
|
|
37
|
+
inserted: 0,
|
|
38
|
+
});
|
|
20
39
|
continue;
|
|
21
40
|
}
|
|
22
|
-
|
|
41
|
+
let entries = [];
|
|
42
|
+
let fetchOk = true;
|
|
43
|
+
try {
|
|
44
|
+
entries = await src.fetch();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
fetchOk = false;
|
|
48
|
+
entries = [];
|
|
49
|
+
}
|
|
50
|
+
// current fingerprints for deletion diff (raw, before dedup)
|
|
51
|
+
const currentFps = new Set();
|
|
52
|
+
for (const e of entries)
|
|
53
|
+
currentFps.add(dedupKey(e));
|
|
23
54
|
let dedupSkipped = 0;
|
|
24
55
|
let inserted = 0;
|
|
25
56
|
for (const e of entries) {
|
|
@@ -29,10 +60,24 @@ export async function idleSync(deps) {
|
|
|
29
60
|
continue;
|
|
30
61
|
}
|
|
31
62
|
seen.add(fp);
|
|
32
|
-
// Insert as memory with source provenance (
|
|
63
|
+
// Insert as memory with source provenance (K21-005: source column)
|
|
64
|
+
// Use relevance_score (not confidence) per schema; source col added in 015.
|
|
65
|
+
// Normalize scope: null → 'project' (schema CHECK)
|
|
66
|
+
const normScope = e.scope ?? "project";
|
|
33
67
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
try {
|
|
69
|
+
deps.store
|
|
70
|
+
.prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source, created_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
|
|
72
|
+
.run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// pre-015 DB without source column: store source in source_tool as fallback
|
|
76
|
+
deps.store
|
|
77
|
+
.prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source_tool, created_at)
|
|
78
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
|
|
79
|
+
.run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
|
|
80
|
+
}
|
|
36
81
|
inserted++;
|
|
37
82
|
}
|
|
38
83
|
catch {
|
|
@@ -43,7 +88,110 @@ export async function idleSync(deps) {
|
|
|
43
88
|
deps.metrics?.incr("source_dedup_skips_total", dedupSkipped);
|
|
44
89
|
if (inserted > 0)
|
|
45
90
|
deps.metrics?.incr("source_syncs_total", 1);
|
|
46
|
-
|
|
91
|
+
// K21-005 — deletion sync: if a previously-saved memory from this source
|
|
92
|
+
// is no longer in the current fetch, archive it (+ tombstone if exported).
|
|
93
|
+
// Gated by source_deletion_sync='1' (opt-in in 2.1.0, D21-03).
|
|
94
|
+
// Only run when fetch succeeded; transient errors must not mass-archive.
|
|
95
|
+
if (fetchOk && isDeletionSyncEnabled(deps.store)) {
|
|
96
|
+
try {
|
|
97
|
+
let prevRows = [];
|
|
98
|
+
try {
|
|
99
|
+
prevRows = deps.store
|
|
100
|
+
.prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source = ? AND status != 'archived' AND fingerprint IS NOT NULL")
|
|
101
|
+
.all(src.name);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// pre-015 DB: source column missing → use source_tool as provenance
|
|
105
|
+
try {
|
|
106
|
+
prevRows = deps.store
|
|
107
|
+
.prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source_tool = ? AND status != 'archived' AND fingerprint IS NOT NULL")
|
|
108
|
+
.all(src.name);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
prevRows = [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const prevFps = new Set();
|
|
115
|
+
const rowByFp = new Map();
|
|
116
|
+
for (const r of prevRows) {
|
|
117
|
+
if (!r.fingerprint)
|
|
118
|
+
continue;
|
|
119
|
+
prevFps.add(r.fingerprint);
|
|
120
|
+
// keep first id per fingerprint
|
|
121
|
+
if (!rowByFp.has(r.fingerprint))
|
|
122
|
+
rowByFp.set(r.fingerprint, { id: r.id, shared_entry_id: r.shared_entry_id, layer: r.layer });
|
|
123
|
+
}
|
|
124
|
+
const deletions = collectDeletions(prevFps, currentFps, src.name);
|
|
125
|
+
for (const d of deletions) {
|
|
126
|
+
const info = rowByFp.get(d.fingerprint);
|
|
127
|
+
if (!info)
|
|
128
|
+
continue;
|
|
129
|
+
// archive locally
|
|
130
|
+
try {
|
|
131
|
+
deps.store
|
|
132
|
+
.prepare("UPDATE memories SET status='archived', archived_at=datetime('now'), updated_at=datetime('now') WHERE id=? AND status!='archived'")
|
|
133
|
+
.run(info.id);
|
|
134
|
+
const ch = deps.store.prepare("SELECT changes() AS c").get();
|
|
135
|
+
if (ch.c === 0)
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// tombstone if ever exported (shared layer)
|
|
142
|
+
const isShared = info.layer === "shared" || info.shared_entry_id !== null;
|
|
143
|
+
if (isShared && deps.sharedLayer && deps.okfPath) {
|
|
144
|
+
try {
|
|
145
|
+
const entryId = info.shared_entry_id;
|
|
146
|
+
if (entryId) {
|
|
147
|
+
const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
|
|
148
|
+
if (plan.write.outcome !== "refused") {
|
|
149
|
+
deps.sharedLayer.applyExport(plan);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// best-effort, never throw
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (!isShared && deps.sharedLayer && deps.okfPath) {
|
|
158
|
+
// For non-shared memories that were previously exported via shared_entries check
|
|
159
|
+
// we attempt tombstone via content-derived entry_id only if a shared entry exists
|
|
160
|
+
try {
|
|
161
|
+
const row = deps.store
|
|
162
|
+
.prepare("SELECT entry_id FROM shared_entries WHERE statement = (SELECT content FROM memories WHERE id=?) LIMIT 1")
|
|
163
|
+
.get(info.id);
|
|
164
|
+
if (row?.entry_id) {
|
|
165
|
+
const plan = deps.sharedLayer.planTombstone([row.entry_id], deps.okfPath);
|
|
166
|
+
if (plan.write.outcome !== "refused")
|
|
167
|
+
deps.sharedLayer.applyExport(plan);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
if (deps.metrics) {
|
|
174
|
+
deps.metrics.incr("source_deletions_total", 1);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
deps.store
|
|
178
|
+
.prepare(`INSERT INTO kevin_metrics (key, value, updated_at) VALUES ('source_deletions_total', 1, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = value + 1, updated_at = datetime('now')`)
|
|
179
|
+
.run();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// best-effort, never break sync
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
results.push({
|
|
190
|
+
source: src.name,
|
|
191
|
+
fetched: entries.length,
|
|
192
|
+
dedupSkipped,
|
|
193
|
+
inserted,
|
|
194
|
+
});
|
|
47
195
|
}
|
|
48
196
|
return results;
|
|
49
197
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MemorySource, SourceEntry } from "./MemorySource.js";
|
|
2
|
+
export declare const NATIVE_CANDIDATE_PATHS: readonly [".opencode/memory/*.md", ".opencode/MEMORY.md"];
|
|
2
3
|
export declare class OpencodeNativeSource implements MemorySource {
|
|
3
4
|
private enabledFlag;
|
|
4
5
|
private projectRoot;
|
|
@@ -7,4 +8,8 @@ export declare class OpencodeNativeSource implements MemorySource {
|
|
|
7
8
|
constructor(enabledFlag: () => boolean, projectRoot?: string);
|
|
8
9
|
enabled(): boolean;
|
|
9
10
|
fetch(): Promise<SourceEntry[]>;
|
|
11
|
+
health(): {
|
|
12
|
+
status: "ok" | "absent";
|
|
13
|
+
detail: string;
|
|
14
|
+
};
|
|
10
15
|
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
// K16-016 — OpencodeNativeSource (native opencode memories, e.g. .opencode/memory)
|
|
2
|
-
|
|
2
|
+
// K21-006 — Relay probe activation: single-source location list, absent-safe
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
export const NATIVE_CANDIDATE_PATHS = [
|
|
6
|
+
".opencode/memory/*.md",
|
|
7
|
+
".opencode/MEMORY.md",
|
|
8
|
+
];
|
|
9
|
+
function globMdFiles(dir) {
|
|
10
|
+
try {
|
|
11
|
+
const entries = readdirSync(dir);
|
|
12
|
+
return entries.filter((f) => f.endsWith(".md")).map((f) => join(dir, f));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
export class OpencodeNativeSource {
|
|
5
19
|
enabledFlag;
|
|
6
20
|
projectRoot;
|
|
@@ -10,24 +24,112 @@ export class OpencodeNativeSource {
|
|
|
10
24
|
this.enabledFlag = enabledFlag;
|
|
11
25
|
this.projectRoot = projectRoot;
|
|
12
26
|
}
|
|
13
|
-
enabled() {
|
|
27
|
+
enabled() {
|
|
28
|
+
return this.enabledFlag();
|
|
29
|
+
}
|
|
14
30
|
async fetch() {
|
|
15
31
|
if (!this.enabled())
|
|
16
32
|
return [];
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
const out = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const pattern of NATIVE_CANDIDATE_PATHS) {
|
|
36
|
+
try {
|
|
37
|
+
if (pattern.includes("*")) {
|
|
38
|
+
// e.g. .opencode/memory/*.md
|
|
39
|
+
const base = pattern.split("*")[0].replace(/\/$/, "");
|
|
40
|
+
const dir = join(this.projectRoot, base);
|
|
41
|
+
let st = null;
|
|
42
|
+
try {
|
|
43
|
+
st = statSync(dir);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!st.isDirectory())
|
|
49
|
+
continue;
|
|
50
|
+
for (const file of globMdFiles(dir)) {
|
|
51
|
+
if (seen.has(file))
|
|
52
|
+
continue;
|
|
53
|
+
seen.add(file);
|
|
54
|
+
try {
|
|
55
|
+
const txt = readFileSync(file, "utf8");
|
|
56
|
+
for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
|
|
57
|
+
if (out.length >= 100)
|
|
58
|
+
break;
|
|
59
|
+
out.push({ statement: line, type: "rule", scope: null, source: this.name });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// absent-safe: skip unreadable file
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const abs = join(this.projectRoot, pattern);
|
|
69
|
+
let st = null;
|
|
70
|
+
try {
|
|
71
|
+
st = statSync(abs);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!st.isFile())
|
|
77
|
+
continue;
|
|
78
|
+
if (seen.has(abs))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(abs);
|
|
81
|
+
try {
|
|
82
|
+
const txt = readFileSync(abs, "utf8");
|
|
83
|
+
for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 100)) {
|
|
84
|
+
if (out.length >= 100)
|
|
85
|
+
break;
|
|
86
|
+
out.push({ statement: line, type: "rule", scope: null, source: this.name });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// skip
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// never throw
|
|
96
|
+
}
|
|
28
97
|
}
|
|
29
|
-
|
|
30
|
-
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
health() {
|
|
101
|
+
let found = 0;
|
|
102
|
+
for (const pattern of NATIVE_CANDIDATE_PATHS) {
|
|
103
|
+
try {
|
|
104
|
+
if (pattern.includes("*")) {
|
|
105
|
+
const base = pattern.split("*")[0].replace(/\/$/, "");
|
|
106
|
+
const dir = join(this.projectRoot, base);
|
|
107
|
+
try {
|
|
108
|
+
const st = statSync(dir);
|
|
109
|
+
if (!st.isDirectory())
|
|
110
|
+
continue;
|
|
111
|
+
found += globMdFiles(dir).length;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const abs = join(this.projectRoot, pattern);
|
|
119
|
+
try {
|
|
120
|
+
const st = statSync(abs);
|
|
121
|
+
if (st.isFile())
|
|
122
|
+
found++;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
31
130
|
}
|
|
131
|
+
if (found > 0)
|
|
132
|
+
return { status: "ok", detail: `found ${found} files` };
|
|
133
|
+
return { status: "absent", detail: "no native memory found at probe paths" };
|
|
32
134
|
}
|
|
33
135
|
}
|
|
@@ -5,7 +5,9 @@ export class OpencodePluginSource {
|
|
|
5
5
|
constructor(enabledFlag) {
|
|
6
6
|
this.enabledFlag = enabledFlag;
|
|
7
7
|
}
|
|
8
|
-
enabled() {
|
|
8
|
+
enabled() {
|
|
9
|
+
return this.enabledFlag();
|
|
10
|
+
}
|
|
9
11
|
async fetch() {
|
|
10
12
|
// Plugin source is the local DB itself — no fetch, handled elsewhere
|
|
11
13
|
return [];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DeletedInfo {
|
|
2
|
+
source: string;
|
|
3
|
+
fingerprint: string;
|
|
4
|
+
file?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Pure diff: fingerprints present in prev but absent from current.
|
|
8
|
+
* Both sets are expected to be non-null; empty sets return [].
|
|
9
|
+
* Malformed inputs (null, non-Set) return [] never throw.
|
|
10
|
+
*/
|
|
11
|
+
export declare function collectDeletions(prevFingerprints: Set<string> | string[] | null | undefined, currentFingerprints: Set<string> | string[] | null | undefined, source: string): DeletedInfo[];
|
|
12
|
+
/**
|
|
13
|
+
* Variant that parses memory_sources.meta_json shape if present.
|
|
14
|
+
* Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
|
|
15
|
+
* For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
|
|
16
|
+
* Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectDeletionsFromMeta(prevMetaJson: string | null, currentFiles: Set<string>, source: string, fingerprintByFile?: Map<string, string>): DeletedInfo[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// K21-005 — Source deletion sync helper
|
|
2
|
+
// Detects memories from a source that have disappeared from the source's current fetch.
|
|
3
|
+
// The source of truth is fingerprints: a memory whose fingerprint is no longer in the
|
|
4
|
+
// fetched set is a candidate for archival + tombstone.
|
|
5
|
+
// File-level meta_json diff (plan §4.3) is not present in v2.0's IdleSync — this layer
|
|
6
|
+
// diffs at fingerprint granularity, which is source-agnostic and cross-file safe.
|
|
7
|
+
// Cross-source protection is enforced by the caller: only memories with matching source
|
|
8
|
+
// are considered deletions.
|
|
9
|
+
/**
|
|
10
|
+
* Pure diff: fingerprints present in prev but absent from current.
|
|
11
|
+
* Both sets are expected to be non-null; empty sets return [].
|
|
12
|
+
* Malformed inputs (null, non-Set) return [] never throw.
|
|
13
|
+
*/
|
|
14
|
+
export function collectDeletions(prevFingerprints, currentFingerprints, source) {
|
|
15
|
+
if (!prevFingerprints || !currentFingerprints)
|
|
16
|
+
return [];
|
|
17
|
+
const prev = prevFingerprints instanceof Set ? prevFingerprints : new Set(prevFingerprints);
|
|
18
|
+
const curr = currentFingerprints instanceof Set ? currentFingerprints : new Set(currentFingerprints);
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const fp of prev) {
|
|
21
|
+
if (typeof fp !== "string" || fp.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
if (!curr.has(fp)) {
|
|
24
|
+
out.push({ source, fingerprint: fp });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Variant that parses memory_sources.meta_json shape if present.
|
|
31
|
+
* Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
|
|
32
|
+
* For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
|
|
33
|
+
* Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
|
|
34
|
+
*/
|
|
35
|
+
export function collectDeletionsFromMeta(prevMetaJson, currentFiles, source, fingerprintByFile) {
|
|
36
|
+
if (!prevMetaJson)
|
|
37
|
+
return [];
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(prevMetaJson);
|
|
40
|
+
if (!parsed || typeof parsed.files !== "object" || parsed.files === null)
|
|
41
|
+
return [];
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const file of Object.keys(parsed.files)) {
|
|
44
|
+
if (!currentFiles.has(file)) {
|
|
45
|
+
const fp = fingerprintByFile?.get(file) ?? file;
|
|
46
|
+
out.push({ source, file, fingerprint: fp });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|