@jmtrin/opencode-kevin 0.9.0 → 1.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/LICENSE +21 -0
- package/README.md +575 -823
- package/dist/migrations/001_initial.sql +91 -91
- package/dist/migrations/002_indexes.sql +13 -13
- package/dist/migrations/003_v02_signal.sql +57 -57
- package/dist/migrations/004_v03_knowledge.sql +138 -138
- package/dist/migrations/005_v04_signal.sql +57 -57
- package/dist/migrations/006_v05_glassbox.sql +118 -118
- package/dist/migrations/007_v06_pull.sql +144 -144
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/migrations/012_v11_drift.sql +24 -0
- package/dist/plugin/Archiver.js +2 -18
- package/dist/plugin/ArtifactWriter.d.ts +25 -10
- package/dist/plugin/ArtifactWriter.js +43 -25
- package/dist/plugin/CausalChain.js +32 -14
- package/dist/plugin/ConflictDetector.js +7 -30
- package/dist/plugin/ContextInjector.js +0 -1
- package/dist/plugin/ConventionMiner.js +0 -1
- package/dist/plugin/Curator.js +0 -1
- package/dist/plugin/Feedback.js +2 -20
- package/dist/plugin/HookLiveness.d.ts +7 -0
- package/dist/plugin/HookLiveness.js +26 -28
- package/dist/plugin/InjectionLedger.js +104 -58
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.js +1 -89
- package/dist/plugin/MemoryService.d.ts +59 -1
- package/dist/plugin/MemoryService.js +13 -107
- package/dist/plugin/Migrate.d.ts +1 -1
- package/dist/plugin/Migrate.js +30 -3
- package/dist/plugin/PatternMiner.js +0 -1
- package/dist/plugin/QualityGate.js +0 -1
- package/dist/plugin/Reflector.js +0 -1
- package/dist/plugin/RepoIdentity.js +0 -1
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +11 -1
- package/dist/plugin/SharedLayer.js +0 -1
- package/dist/plugin/Store.js +0 -1
- package/dist/plugin/ToolCallObserver.js +18 -6
- package/dist/plugin/capabilities.js +0 -1
- package/dist/plugin/columns.d.ts +11 -0
- package/dist/plugin/columns.js +54 -0
- package/dist/plugin/confidence.js +0 -1
- package/dist/plugin/contract.d.ts +54 -0
- package/dist/plugin/contract.js +383 -0
- package/dist/plugin/diff.js +0 -1
- package/dist/plugin/escape.d.ts +39 -0
- package/dist/plugin/escape.js +60 -0
- package/dist/plugin/fingerprint.js +0 -1
- package/dist/plugin/host.js +0 -1
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +448 -229
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +31 -0
- package/dist/plugin/kevin_audit.js +60 -1
- package/dist/plugin/kevin_bench.d.ts +10 -0
- package/dist/plugin/kevin_bench.js +82 -0
- package/dist/plugin/kevin_conflicts.js +0 -1
- package/dist/plugin/kevin_contract.d.ts +8 -0
- package/dist/plugin/kevin_contract.js +59 -0
- package/dist/plugin/kevin_doctor.d.ts +5 -0
- package/dist/plugin/kevin_doctor.js +30 -2
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_forget.d.ts +33 -0
- package/dist/plugin/kevin_forget.js +260 -0
- package/dist/plugin/kevin_native.js +0 -1
- package/dist/plugin/kevin_propose.js +0 -1
- package/dist/plugin/kevin_publish.js +0 -1
- package/dist/plugin/kevin_why.js +1 -19
- package/dist/plugin/memory-format.js +0 -1
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +4 -1
- package/dist/plugin/native.js +0 -1
- package/dist/plugin/okf-export.js +0 -1
- package/dist/plugin/okf-import.js +0 -1
- package/dist/plugin/okf.js +0 -1
- package/dist/plugin/perf.d.ts +35 -0
- package/dist/plugin/perf.js +174 -0
- package/dist/plugin/query-tokenizer.js +56 -9
- package/dist/plugin/redact.js +0 -1
- package/dist/plugin/replay-types.js +0 -1
- package/dist/plugin/replay.js +0 -1
- package/dist/plugin/sqlite-adapter.js +0 -1
- package/dist/plugin/time-ms.d.ts +1 -0
- package/dist/plugin/time-ms.js +16 -0
- package/dist/plugin/uuid.js +0 -1
- package/package.json +22 -6
- package/dist/plugin/Archiver.js.map +0 -1
- package/dist/plugin/ArtifactWriter.js.map +0 -1
- package/dist/plugin/CausalChain.js.map +0 -1
- package/dist/plugin/ConflictDetector.js.map +0 -1
- package/dist/plugin/ContextInjector.js.map +0 -1
- package/dist/plugin/ConventionMiner.js.map +0 -1
- package/dist/plugin/Curator.js.map +0 -1
- package/dist/plugin/Feedback.js.map +0 -1
- package/dist/plugin/HookLiveness.js.map +0 -1
- package/dist/plugin/InjectionLedger.js.map +0 -1
- package/dist/plugin/LessonFixer.js.map +0 -1
- package/dist/plugin/Materializer.js.map +0 -1
- package/dist/plugin/MemoryService.js.map +0 -1
- package/dist/plugin/Migrate.js.map +0 -1
- package/dist/plugin/PatternMiner.js.map +0 -1
- package/dist/plugin/QualityGate.js.map +0 -1
- package/dist/plugin/Reflector.js.map +0 -1
- package/dist/plugin/RepoIdentity.js.map +0 -1
- package/dist/plugin/RepoTruth.js.map +0 -1
- package/dist/plugin/Retrospective.js.map +0 -1
- package/dist/plugin/SharedLayer.js.map +0 -1
- package/dist/plugin/Store.js.map +0 -1
- package/dist/plugin/ToolCallObserver.js.map +0 -1
- package/dist/plugin/capabilities.js.map +0 -1
- package/dist/plugin/confidence.js.map +0 -1
- package/dist/plugin/diff.js.map +0 -1
- package/dist/plugin/fingerprint.js.map +0 -1
- package/dist/plugin/host.js.map +0 -1
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/inferability.js.map +0 -1
- package/dist/plugin/kevin_approve.js.map +0 -1
- package/dist/plugin/kevin_audit.js.map +0 -1
- package/dist/plugin/kevin_conflicts.js.map +0 -1
- package/dist/plugin/kevin_doctor.js.map +0 -1
- package/dist/plugin/kevin_facts.js.map +0 -1
- package/dist/plugin/kevin_native.js.map +0 -1
- package/dist/plugin/kevin_propose.js.map +0 -1
- package/dist/plugin/kevin_publish.js.map +0 -1
- package/dist/plugin/kevin_why.js.map +0 -1
- package/dist/plugin/memory-format.js.map +0 -1
- package/dist/plugin/metrics.js.map +0 -1
- package/dist/plugin/native.js.map +0 -1
- package/dist/plugin/okf-export.js.map +0 -1
- package/dist/plugin/okf-import.js.map +0 -1
- package/dist/plugin/okf.js.map +0 -1
- package/dist/plugin/query-tokenizer.js.map +0 -1
- package/dist/plugin/redact.js.map +0 -1
- package/dist/plugin/replay-types.js.map +0 -1
- package/dist/plugin/replay.js.map +0 -1
- package/dist/plugin/sqlite-adapter.js.map +0 -1
- package/dist/plugin/uuid.js.map +0 -1
- package/migrations/001_initial.sql +0 -92
- package/migrations/002_indexes.sql +0 -14
- package/migrations/003_v02_signal.sql +0 -58
- package/migrations/004_v03_knowledge.sql +0 -138
- package/migrations/005_v04_signal.sql +0 -57
- package/migrations/006_v05_glassbox.sql +0 -118
- package/migrations/007_v06_pull.sql +0 -145
- package/migrations/008_v07_truth.sql +0 -124
- package/migrations/009_v08_team.sql +0 -100
- package/migrations/010_v09_native.sql +0 -78
package/dist/plugin/index.js
CHANGED
|
@@ -29,9 +29,12 @@ import { computeConfidence } from "./confidence.js";
|
|
|
29
29
|
import { probeHost, summarize } from "./host.js";
|
|
30
30
|
import { kevinApprove } from "./kevin_approve.js";
|
|
31
31
|
import { buildAudit } from "./kevin_audit.js";
|
|
32
|
+
import { buildKevinBench } from "./kevin_bench.js";
|
|
32
33
|
import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
34
|
+
import { buildKevinContract } from "./kevin_contract.js";
|
|
33
35
|
import { buildDoctor } from "./kevin_doctor.js";
|
|
34
36
|
import { buildKevinFacts } from "./kevin_facts.js";
|
|
37
|
+
import { handleForget } from "./kevin_forget.js";
|
|
35
38
|
import { handleNative } from "./kevin_native.js";
|
|
36
39
|
import { kevinPropose } from "./kevin_propose.js";
|
|
37
40
|
import { kevinPublish } from "./kevin_publish.js";
|
|
@@ -40,6 +43,7 @@ import { Metrics } from "./metrics.js";
|
|
|
40
43
|
import { attachNative } from "./native.js";
|
|
41
44
|
import { exportMarkdown, exportOkf } from "./okf-export.js";
|
|
42
45
|
import { importOkf } from "./okf-import.js";
|
|
46
|
+
import { Perf } from "./perf.js";
|
|
43
47
|
import { uuidv7 } from "./uuid.js";
|
|
44
48
|
// v0.6.0 (K6-019) — process-global set of reference topics already
|
|
45
49
|
// registered with the host this process. Keeps re-registration idempotent
|
|
@@ -101,7 +105,15 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
101
105
|
"native_registration_enabled",
|
|
102
106
|
"host_probe_history_enabled",
|
|
103
107
|
"dead_hook_report_threshold",
|
|
108
|
+
// v1.0.0 (K10-005 / plan §6) — the four keys seeded by migration 011
|
|
109
|
+
// section 4. Omitting these makes `kevin_config set` return
|
|
110
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
111
|
+
"perf_enabled",
|
|
112
|
+
"perf_ring_capacity",
|
|
113
|
+
"perf_flush_on_idle",
|
|
114
|
+
"contract_report_enabled",
|
|
104
115
|
];
|
|
116
|
+
// v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
|
|
105
117
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
106
118
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
107
119
|
// `=== "triage_only"`, never by truthiness; the domain here is enforced by
|
|
@@ -110,7 +122,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
110
122
|
// behaviour on the next reflection.
|
|
111
123
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
112
124
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
113
|
-
export const KEVIN_VERSION = "
|
|
125
|
+
export const KEVIN_VERSION = "1.1.0";
|
|
114
126
|
function resolveMigrationsDir() {
|
|
115
127
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
116
128
|
return join(here, "..", "migrations");
|
|
@@ -292,6 +304,15 @@ export const KevinPlugin = async (input, options) => {
|
|
|
292
304
|
thresholdText: memoryService.getSetting("dead_hook_report_threshold", "3"),
|
|
293
305
|
pluginVersion: host.pluginVersion,
|
|
294
306
|
});
|
|
307
|
+
// v1.0.0 (K10-012 / plan §5.2) — the performance instrument. Measures
|
|
308
|
+
// how long each hook holds the host into per-scope ring buffers that
|
|
309
|
+
// only reach the store at session.idle (D10-11: no perf_samples write
|
|
310
|
+
// anywhere else). perf_enabled uses the explicit === "1" TEXT
|
|
311
|
+
// comparison; the capacity string goes through the parse guard/clamp.
|
|
312
|
+
const perf = new Perf({
|
|
313
|
+
enabled: memoryService.getSetting("perf_enabled", "1") === "1",
|
|
314
|
+
capacity: memoryService.getSetting("perf_ring_capacity", "") ?? "",
|
|
315
|
+
});
|
|
295
316
|
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
296
317
|
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
297
318
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
@@ -451,6 +472,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
451
472
|
}
|
|
452
473
|
}
|
|
453
474
|
let currentSessionId = null;
|
|
475
|
+
// v1.0.0 (K10-013 / D10-08) — set on the first tool completion of a
|
|
476
|
+
// session; consumed at idle to arm the deferred-dispose marker.
|
|
477
|
+
let sessionRecordedWork = false;
|
|
454
478
|
// BUG-011 — process-global last derived query. Cleared on
|
|
455
479
|
// `session.idle`; the per-session map below is the preferred source.
|
|
456
480
|
let lastUserQuery = null;
|
|
@@ -848,8 +872,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
848
872
|
// monotone across releases; K6-024 extends this
|
|
849
873
|
// block with the remaining v0.6 fields. v0.8.0
|
|
850
874
|
// (K8-025) — 18 v0.7 + kevin_project +
|
|
851
|
-
// kevin_native = 23.
|
|
852
|
-
|
|
875
|
+
// kevin_native = 23. v1.0.0 (K10-018) — +kevin_contract +kevin_bench = 25.
|
|
876
|
+
// v1.1.0 (K11-007) — +kevin_forget = 26.
|
|
877
|
+
tool_count: 26,
|
|
853
878
|
v07,
|
|
854
879
|
memories_reflector: memoriesReflector,
|
|
855
880
|
memories_agent: memoriesAgent,
|
|
@@ -1079,6 +1104,49 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1079
1104
|
};
|
|
1080
1105
|
},
|
|
1081
1106
|
}),
|
|
1107
|
+
// v1.0.0 (K10-018 / plan §5.6) — kevin_contract: the frozen
|
|
1108
|
+
// surface, inspectable at runtime rather than only at test
|
|
1109
|
+
// time. Read-only; summary by default, one clause's full value
|
|
1110
|
+
// with clause+format:'full'. Unknown clause ids are a
|
|
1111
|
+
// structured error, never a throw.
|
|
1112
|
+
kevin_contract: tool({
|
|
1113
|
+
description: "Contrato publico de Kevin (v1.0.0): version, digest y por clausula id/titulo/estabilidad/since/deprecacion. clause:'C-0N' con format:'full' retorna el valor completo de esa clausula (marcadores, tool names, settings keys, metric keys, entry points, schema, invariantes). Solo lectura, sin LLM ni red.",
|
|
1114
|
+
args: {
|
|
1115
|
+
clause: tool.schema
|
|
1116
|
+
.string()
|
|
1117
|
+
.optional()
|
|
1118
|
+
.describe("Id de clausula (ej. 'C-01'). Sin clause: resumen."),
|
|
1119
|
+
format: tool.schema
|
|
1120
|
+
.enum(["summary", "full"])
|
|
1121
|
+
.optional()
|
|
1122
|
+
.describe("summary (default): una linea por clausula. full: valores completos (requiere clause para una clausula; solo devuelve resumen enriquecido si se omite)."),
|
|
1123
|
+
},
|
|
1124
|
+
async execute(args) {
|
|
1125
|
+
const result = buildKevinContract({ packageVersion: KEVIN_VERSION }, args);
|
|
1126
|
+
return {
|
|
1127
|
+
title: "Contrato de Kevin",
|
|
1128
|
+
output: JSON.stringify(result),
|
|
1129
|
+
};
|
|
1130
|
+
},
|
|
1131
|
+
}),
|
|
1132
|
+
// v1.0.0 (K10-019 / plan §5.6) — kevin_bench: reports what
|
|
1133
|
+
// `npm run bench` recorded in bench_runs. It NEVER runs the
|
|
1134
|
+
// benchmark from inside a live session.
|
|
1135
|
+
kevin_bench: tool({
|
|
1136
|
+
description: "Resultados del benchmark de retrieval (v1.0.0). status: si hay corridas, digest del corpus mas reciente y si coincide con el corpus en disco. last: los cuatro brazos (none/recent-k/random-k/kevin) con precision@5, recall@5 y MRR de la ultima corrida. NUNCA ejecuta el benchmark — corre 'npm run bench' fuera de la sesion.",
|
|
1137
|
+
args: {
|
|
1138
|
+
action: tool.schema
|
|
1139
|
+
.enum(["status", "last"])
|
|
1140
|
+
.describe("status | last"),
|
|
1141
|
+
},
|
|
1142
|
+
async execute(args) {
|
|
1143
|
+
const result = buildKevinBench({ store }, args);
|
|
1144
|
+
return {
|
|
1145
|
+
title: "Benchmark de Kevin",
|
|
1146
|
+
output: JSON.stringify(result),
|
|
1147
|
+
};
|
|
1148
|
+
},
|
|
1149
|
+
}),
|
|
1082
1150
|
kevin_retrospective: tool({
|
|
1083
1151
|
description: "Genera un retrospective markdown para una sesion (resume tools que fallaron y lecciones aprendidas).",
|
|
1084
1152
|
args: {
|
|
@@ -1396,6 +1464,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1396
1464
|
};
|
|
1397
1465
|
},
|
|
1398
1466
|
}),
|
|
1467
|
+
// v1.1.0 (K11-007 / plan §5.1, D11-02) — kevin_forget: closes the sharing lifecycle.
|
|
1468
|
+
kevin_forget: tool({
|
|
1469
|
+
description: "Olvida memorias y publica tombstones en la capa compartida (v1.1.0, K11-005/006 / plan §5.1): dry-run por defecto — sin confirm no muta nada y devuelve el plan (archived, tombstone planned); con confirm:true archiva localmente (status='archived') y, cuando la memoria proyecta a la capa compartida (layer='shared' o shared_entry_id), publica un tombstone via el unico write path (SharedLayer.applyExport, D8-08). Segunda invocacion identica reporta noop.",
|
|
1470
|
+
args: {
|
|
1471
|
+
ids: tool.schema.array(tool.schema.string()).min(1),
|
|
1472
|
+
confirm: tool.schema.boolean().optional(),
|
|
1473
|
+
},
|
|
1474
|
+
async execute(args) {
|
|
1475
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
1476
|
+
const result = handleForget({ ids: args.ids, confirm: args.confirm }, { store, memoryService, sharedLayer, okfPath, metrics });
|
|
1477
|
+
return {
|
|
1478
|
+
title: result.dry_run
|
|
1479
|
+
? "Plan de olvido (dry-run)"
|
|
1480
|
+
: "Olvido aplicado",
|
|
1481
|
+
output: JSON.stringify(result),
|
|
1482
|
+
};
|
|
1483
|
+
},
|
|
1484
|
+
}),
|
|
1399
1485
|
kevin_approve: tool({
|
|
1400
1486
|
description: "Aprueba o rechaza una propuesta de curacion (v0.6.0). reject: marca rejected, nada toca disco. approve: aplica el diff (unico call site de ArtifactWriter.apply, D6-01), marca applied y cura las memorias contribuyentes. Solo acepta propuestas pending.",
|
|
1401
1487
|
args: {
|
|
@@ -1519,267 +1605,400 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1519
1605
|
}),
|
|
1520
1606
|
},
|
|
1521
1607
|
"tool.execute.before": async (hookInput, output) => {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1608
|
+
// v1.0.0 (K10-012) — perf measures the synchronous hold time;
|
|
1609
|
+
// HookLiveness wraps the outside of this handler (compose,
|
|
1610
|
+
// never merge — D10-09).
|
|
1611
|
+
perf.measure("tool.execute.before", () => {
|
|
1612
|
+
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
1613
|
+
observer.onBefore({
|
|
1614
|
+
tool: hookInput.tool,
|
|
1615
|
+
args: output.args,
|
|
1616
|
+
sessionId: hookInput.sessionID,
|
|
1617
|
+
callID: hookInput.callID,
|
|
1618
|
+
projectId,
|
|
1619
|
+
}, {});
|
|
1620
|
+
});
|
|
1530
1621
|
},
|
|
1531
1622
|
"tool.execute.after": async (hookInput, output) => {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
success
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
projectId,
|
|
1564
|
-
}, { success, stdout, stderr, exitCode });
|
|
1565
|
-
if (!success) {
|
|
1566
|
-
const errorType = observer.inferErrorType(stderr, stdout, exitCode);
|
|
1567
|
-
fireAndForget(reflector.invoke({
|
|
1568
|
-
toolName: hookInput.tool,
|
|
1569
|
-
argsSummary: observer.summarizeArgs(hookInput.args),
|
|
1570
|
-
stderr,
|
|
1571
|
-
stdout,
|
|
1572
|
-
exitCode,
|
|
1573
|
-
errorType,
|
|
1623
|
+
perf.measure("tool.execute.after", () => {
|
|
1624
|
+
sessionRecordedWork = true;
|
|
1625
|
+
// v0.9.0 (K9-010 / plan §5.3) — checkpoint: the session reached a
|
|
1626
|
+
// model turn, so the system prompt was assembled and
|
|
1627
|
+
// `experimental.chat.system.transform` MUST have been offered.
|
|
1628
|
+
// Deduped per session inside HookLiveness.expect.
|
|
1629
|
+
if (hookInput.sessionID) {
|
|
1630
|
+
liveness.expect("experimental.chat.system.transform", hookInput.sessionID);
|
|
1631
|
+
}
|
|
1632
|
+
const meta = (output.metadata ?? {});
|
|
1633
|
+
const outputText = String(output.output ?? "");
|
|
1634
|
+
const stderr = String(meta.stderr ?? "");
|
|
1635
|
+
const stdout = String(meta.stdout ?? outputText);
|
|
1636
|
+
const exitCode = pickExitCode(meta);
|
|
1637
|
+
let success;
|
|
1638
|
+
if (meta.success === false) {
|
|
1639
|
+
success = false;
|
|
1640
|
+
}
|
|
1641
|
+
else if (exitCode !== undefined) {
|
|
1642
|
+
success = exitCode === 0;
|
|
1643
|
+
}
|
|
1644
|
+
else if (stderr.length > 0 && ERROR_LINE_RE.test(stderr)) {
|
|
1645
|
+
success = false;
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
const stream = stdout.length > 0 ? stdout : outputText;
|
|
1649
|
+
success = !(stream.length > 0 && STRONG_ERROR_RE.test(stream));
|
|
1650
|
+
}
|
|
1651
|
+
observer.onAfter({
|
|
1652
|
+
tool: hookInput.tool,
|
|
1653
|
+
args: hookInput.args,
|
|
1574
1654
|
sessionId: hookInput.sessionID,
|
|
1575
1655
|
callID: hookInput.callID,
|
|
1576
1656
|
projectId,
|
|
1577
|
-
})
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1657
|
+
}, { success, stdout, stderr, exitCode });
|
|
1658
|
+
if (!success) {
|
|
1659
|
+
const errorType = observer.inferErrorType(stderr, stdout, exitCode);
|
|
1660
|
+
fireAndForget(reflector.invoke({
|
|
1661
|
+
toolName: hookInput.tool,
|
|
1662
|
+
argsSummary: observer.summarizeArgs(hookInput.args),
|
|
1663
|
+
stderr,
|
|
1664
|
+
stdout,
|
|
1665
|
+
exitCode,
|
|
1666
|
+
errorType,
|
|
1667
|
+
sessionId: hookInput.sessionID,
|
|
1668
|
+
callID: hookInput.callID,
|
|
1669
|
+
projectId,
|
|
1670
|
+
}));
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
causalChain.onSuccess(hookInput.tool, hookInput.args, projectId, hookInput.sessionID);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1582
1676
|
},
|
|
1583
1677
|
"chat.message": async (hookInput, output) => {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1678
|
+
perf.measure("chat.message", () => {
|
|
1679
|
+
const text = output.parts
|
|
1680
|
+
.map((p) => p)
|
|
1681
|
+
.filter((p) => p.type === "text")
|
|
1682
|
+
.map((p) => p.text ?? "")
|
|
1683
|
+
.join(" ");
|
|
1684
|
+
if (text.trim()) {
|
|
1685
|
+
const derived = injector.deriveQuery([
|
|
1686
|
+
{ role: "user", content: text },
|
|
1687
|
+
]);
|
|
1688
|
+
lastUserQuery = derived.length > 0 ? derived : null;
|
|
1689
|
+
if (hookInput.sessionID && lastUserQuery) {
|
|
1690
|
+
lastUserQueryBySession.set(hookInput.sessionID, lastUserQuery);
|
|
1691
|
+
}
|
|
1594
1692
|
}
|
|
1595
|
-
}
|
|
1693
|
+
});
|
|
1596
1694
|
},
|
|
1597
1695
|
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1696
|
+
perf.measure("chat.system.transform", () => {
|
|
1697
|
+
// BUG-011 — prefer the per-session query so a new session whose
|
|
1698
|
+
// first transform fires before any chat.message cannot reuse the
|
|
1699
|
+
// previous session's query (the global is cleared on idle).
|
|
1700
|
+
const query = lastUserQueryBySession.get(hookInput.sessionID ?? "") ??
|
|
1701
|
+
lastUserQuery;
|
|
1702
|
+
if (!query)
|
|
1703
|
+
return;
|
|
1704
|
+
const suggestion = injector.generateSuggestion();
|
|
1705
|
+
if (suggestion)
|
|
1706
|
+
output.system.push(suggestion);
|
|
1707
|
+
injector.onSystemTransform({
|
|
1708
|
+
sessionID: hookInput.sessionID ?? undefined,
|
|
1709
|
+
messages: [{ role: "user", content: query }],
|
|
1710
|
+
}, output);
|
|
1711
|
+
});
|
|
1611
1712
|
},
|
|
1612
1713
|
"experimental.session.compacting": async (hookInput, output) => {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1714
|
+
perf.measure("session.compacting", () => {
|
|
1715
|
+
// v0.4.0 (K4-018) — plan §5.6: compaction often fires with no
|
|
1716
|
+
// recent chat.message (auto-compact after a long tool turn,
|
|
1717
|
+
// resumed sessions). Resolve a query per session first, then
|
|
1718
|
+
// the global fallback, then any messages the runtime may
|
|
1719
|
+
// provide (defensive — the current SDK contract only exposes
|
|
1720
|
+
// sessionID).
|
|
1721
|
+
// BUG-012 — the HITL suggestion fires AT MOST ONCE per session:
|
|
1722
|
+
// whichever hook (transform or compacting) runs first consumes
|
|
1723
|
+
// the pending recurrence signal (generateSuggestion resets it).
|
|
1724
|
+
const suggestion = injector.generateSuggestion();
|
|
1725
|
+
if (suggestion)
|
|
1726
|
+
output.context.push(suggestion);
|
|
1727
|
+
const sid = hookInput.sessionID;
|
|
1728
|
+
const sessionQuery = lastUserQueryBySession.get(sid) ?? lastUserQuery;
|
|
1729
|
+
const runtimeMessages = hookInput
|
|
1730
|
+
.messages;
|
|
1731
|
+
const messages = sessionQuery != null
|
|
1732
|
+
? [{ role: "user", content: sessionQuery }]
|
|
1733
|
+
: (runtimeMessages ?? []);
|
|
1734
|
+
injector.onCompacting({
|
|
1735
|
+
sessionID: sid,
|
|
1736
|
+
messages,
|
|
1737
|
+
}, output);
|
|
1738
|
+
});
|
|
1636
1739
|
},
|
|
1637
1740
|
event: async ({ event }) => {
|
|
1638
1741
|
const type = event.type;
|
|
1639
1742
|
const props = event.properties ?? {};
|
|
1640
|
-
if (type === "session.
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
// still hold it if no idle fired).
|
|
1647
|
-
lastUserQueryBySession.delete(info.id);
|
|
1648
|
-
// v0.4.0 (K4-017) — plan §5.1 rule 3: the per-session
|
|
1649
|
-
// seen-set resets when a session is created.
|
|
1650
|
-
injector.onSessionCreated(info.id);
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
else if (type === "session.idle") {
|
|
1743
|
+
if (type === "session.idle") {
|
|
1744
|
+
// v1.0.0 (K10-012) — the idle branch measures under
|
|
1745
|
+
// "session.idle" (150/600 ms budget); every other branch
|
|
1746
|
+
// measures under "event" (5/25 ms). Recording idle's ~150 ms
|
|
1747
|
+
// under "event" would make that budget permanently breached
|
|
1748
|
+
// and therefore ignored.
|
|
1654
1749
|
const sid = props.sessionID;
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1750
|
+
await perf.measureAsync("session.idle", async () => {
|
|
1751
|
+
if (sid) {
|
|
1752
|
+
toolCache.clear();
|
|
1753
|
+
// BUG-011 — the session is done: drop the global query
|
|
1754
|
+
// so the next session cannot reuse it.
|
|
1755
|
+
lastUserQuery = null;
|
|
1756
|
+
// v0.4.0 (K4-024) — plan §5.2: settle the session's
|
|
1757
|
+
// unmeasured injections (effective/ineffective +
|
|
1758
|
+
// recurrence_count charges) at idle. Best-effort: a
|
|
1759
|
+
// legacy DB without migration 005 has no ledger table.
|
|
1760
|
+
try {
|
|
1761
|
+
ledger.settle(sid);
|
|
1762
|
+
}
|
|
1763
|
+
catch {
|
|
1764
|
+
// best-effort: a legacy DB without the ledger
|
|
1765
|
+
// table must not break the idle path
|
|
1766
|
+
}
|
|
1767
|
+
// v0.5.0 (K5-012 / plan §5.4) — retire stale lessons at
|
|
1768
|
+
// idle; pre-006 DBs degrade to a no-op.
|
|
1769
|
+
try {
|
|
1770
|
+
archiver.run();
|
|
1771
|
+
}
|
|
1772
|
+
catch {
|
|
1773
|
+
// best-effort, same pattern as ledger.settle
|
|
1774
|
+
}
|
|
1775
|
+
fireAndForget(retrospective.generate(sid));
|
|
1776
|
+
memoryService.boostPositiveReflectors(sid);
|
|
1777
|
+
const recurred = memoryService.penalizeRecurringReflectors(sid);
|
|
1778
|
+
injector.setRecurrences(recurred, sid);
|
|
1779
|
+
patternMiner.mine(projectId);
|
|
1780
|
+
// v0.7.0 (K7-012 / plan §5.4, D7-10) — convention mining is
|
|
1781
|
+
// session.idle-only, default-off, and isolated from the rest of
|
|
1782
|
+
// the idle chain. Mined rules still enter the ordinary Curator
|
|
1783
|
+
// approval path; this step never writes to the repository.
|
|
1784
|
+
try {
|
|
1785
|
+
if (memoryService.getSetting("convention_mining_enabled", "0") ===
|
|
1786
|
+
"1") {
|
|
1787
|
+
const conventions = conventionMiner.mine();
|
|
1788
|
+
conventionMiner.emit(conventions);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
catch {
|
|
1792
|
+
// A throwing miner must not reject or truncate the idle chain.
|
|
1793
|
+
}
|
|
1794
|
+
// v0.7.0 (K7-016 / plan §5.5, D7-06) — conflict detection is
|
|
1795
|
+
// surfacing-only on idle. It may create/open conflict rows, but
|
|
1796
|
+
// no idle path can acknowledge or resolve one.
|
|
1797
|
+
try {
|
|
1798
|
+
if (memoryService.getSetting("conflict_detection_enabled", "0") ===
|
|
1799
|
+
"1") {
|
|
1800
|
+
conflictDetector.detect();
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
catch {
|
|
1804
|
+
// Conflict surfacing is best-effort and must not reject idle.
|
|
1805
|
+
}
|
|
1806
|
+
fireAndForget(Promise.resolve()
|
|
1807
|
+
.then(() => causalChain.onSessionIdle(sid))
|
|
1808
|
+
// non-blocking — promote is a best-effort pass
|
|
1809
|
+
// (legacy DBs pre-005 lack the recurrence_count
|
|
1810
|
+
// column)
|
|
1811
|
+
.catch(() => { }));
|
|
1812
|
+
// v0.6.0 (K6-015 / plan §8.14) — session-idle curation
|
|
1813
|
+
// generation. Dry-run only: `propose()` calls `plan()`
|
|
1814
|
+
// and never `apply()`, so nothing here can touch disk
|
|
1815
|
+
// (D6-01). Guarded by curation_enabled (TEXT compare)
|
|
1816
|
+
// and a 1-hour throttle persisted in kevin_settings.
|
|
1817
|
+
try {
|
|
1818
|
+
if (memoryService.getSetting("curation_enabled", "1") === "1") {
|
|
1819
|
+
const CURATION_THROTTLE_MS = 3_600_000;
|
|
1820
|
+
const last = memoryService.getSetting("last_curation_at", "");
|
|
1821
|
+
if (last === "" ||
|
|
1822
|
+
Date.now() - Date.parse(last) > CURATION_THROTTLE_MS) {
|
|
1823
|
+
curator.propose("agents_md", writer);
|
|
1824
|
+
store
|
|
1825
|
+
.prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
|
|
1826
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
1827
|
+
.run("last_curation_at", new Date().toISOString());
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
catch {
|
|
1832
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1833
|
+
// curation failure must not break the idle path
|
|
1834
|
+
}
|
|
1835
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer
|
|
1836
|
+
// re-read at idle. Gated by shared_layer_enabled ===
|
|
1837
|
+
// "1", a TEXT comparison: '0' is a truthy string, so a
|
|
1838
|
+
// truthiness check would turn the release on for every
|
|
1839
|
+
// installation that upgrades. Never wired into
|
|
1840
|
+
// tool.execute.*, chat.message, system.transform or
|
|
1841
|
+
// session.compacting — the hot-path rule is absolute;
|
|
1842
|
+
// the file-hash skip is what makes the idle cost one
|
|
1843
|
+
// read plus one hash on an unchanged repository.
|
|
1844
|
+
try {
|
|
1845
|
+
if (memoryService.getSetting("shared_layer_enabled", "0") === "1") {
|
|
1846
|
+
syncSharedLayer();
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
catch {
|
|
1850
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1851
|
+
// sync failure must not break the idle path
|
|
1852
|
+
}
|
|
1853
|
+
// v1.0.0 (K10-013 / D10-08) — the session recorded work:
|
|
1854
|
+
// arm the deferred dispose settlement. The ISO timestamp
|
|
1855
|
+
// lets the next session.start compare it against
|
|
1856
|
+
// hook_liveness.last_seen_at for dispose.
|
|
1857
|
+
if (sessionRecordedWork) {
|
|
1858
|
+
try {
|
|
1859
|
+
store
|
|
1860
|
+
.prepare("INSERT INTO kevin_settings (key, value) VALUES ('last_session_recorded_work', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
1861
|
+
.run(new Date().toISOString());
|
|
1862
|
+
}
|
|
1863
|
+
catch {
|
|
1864
|
+
// best-effort: arming must not break idle
|
|
1865
|
+
}
|
|
1866
|
+
sessionRecordedWork = false;
|
|
1692
1867
|
}
|
|
1693
1868
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1869
|
+
});
|
|
1870
|
+
metrics.flush();
|
|
1871
|
+
// v1.0.0 (K10-012 / plan §5.2, D10-11) — the idle perf_samples
|
|
1872
|
+
// write (the dispose hook holds the other one), gated by
|
|
1873
|
+
// perf_flush_on_idle (TEXT compare; the fallback matches the
|
|
1874
|
+
// migration-seeded default '1').
|
|
1875
|
+
if (memoryService.getSetting("perf_flush_on_idle", "1") === "1") {
|
|
1700
1876
|
try {
|
|
1701
|
-
|
|
1702
|
-
"1") {
|
|
1703
|
-
conflictDetector.detect();
|
|
1704
|
-
}
|
|
1877
|
+
perf.flush(store);
|
|
1705
1878
|
}
|
|
1706
1879
|
catch {
|
|
1707
|
-
//
|
|
1880
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1708
1881
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1882
|
+
}
|
|
1883
|
+
liveness.flush();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
perf.measure("event", () => {
|
|
1887
|
+
if (type === "session.created") {
|
|
1888
|
+
const info = props.info;
|
|
1889
|
+
if (info?.id) {
|
|
1890
|
+
currentSessionId = info.id;
|
|
1891
|
+
// BUG-011 — a fresh session must not inherit the
|
|
1892
|
+
// previous session's derived query (the global may
|
|
1893
|
+
// still hold it if no idle fired).
|
|
1894
|
+
lastUserQueryBySession.delete(info.id);
|
|
1895
|
+
// v0.4.0 (K4-017) — plan §5.1 rule 3: the per-session
|
|
1896
|
+
// seen-set resets when a session is created.
|
|
1897
|
+
injector.onSessionCreated(info.id);
|
|
1898
|
+
// v1.0.0 (K10-013 / plan §5.3, D10-08) — deferred
|
|
1899
|
+
// dispose settlement. The previous session recorded
|
|
1900
|
+
// work but the process never came back through
|
|
1901
|
+
// `dispose` (crash or hard kill): settle it HERE,
|
|
1902
|
+
// at the start of the next session, because dispose
|
|
1903
|
+
// cannot be settled within the session that observes
|
|
1904
|
+
// it. One settlement per work marker; a first-ever
|
|
1905
|
+
// run has no marker and never reports dead. The
|
|
1906
|
+
// threshold semantics stay with expect(): unknown
|
|
1907
|
+
// until expected_count crosses the threshold.
|
|
1908
|
+
try {
|
|
1909
|
+
const marker = memoryService.getSetting("last_session_recorded_work", "");
|
|
1910
|
+
if (marker !== "") {
|
|
1911
|
+
let fired = true;
|
|
1912
|
+
try {
|
|
1913
|
+
const row = store
|
|
1914
|
+
.prepare("SELECT last_seen_at FROM hook_liveness WHERE hook = 'dispose'")
|
|
1915
|
+
.get();
|
|
1916
|
+
const last = row?.last_seen_at ?? null;
|
|
1917
|
+
fired = last !== null && last > marker;
|
|
1918
|
+
}
|
|
1919
|
+
catch {
|
|
1920
|
+
// pre-010 DB without hook_liveness: treat as
|
|
1921
|
+
// no evidence of a fire, same as a crash.
|
|
1922
|
+
fired = false;
|
|
1923
|
+
}
|
|
1924
|
+
if (!fired) {
|
|
1925
|
+
liveness.expect("dispose", `miss:${marker}`);
|
|
1926
|
+
store
|
|
1927
|
+
.prepare("UPDATE kevin_metrics SET value = value + 1 WHERE key = 'dispose_misses_total'")
|
|
1928
|
+
.run();
|
|
1929
|
+
}
|
|
1727
1930
|
store
|
|
1728
|
-
.prepare(
|
|
1729
|
-
|
|
1730
|
-
.run("last_curation_at", new Date().toISOString());
|
|
1931
|
+
.prepare("UPDATE kevin_settings SET value = '' WHERE key = 'last_session_recorded_work'")
|
|
1932
|
+
.run();
|
|
1731
1933
|
}
|
|
1732
1934
|
}
|
|
1935
|
+
catch {
|
|
1936
|
+
// best-effort: settlement must not break session start
|
|
1937
|
+
}
|
|
1733
1938
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1939
|
+
}
|
|
1940
|
+
else if (type === "session.next.tool.failed") {
|
|
1941
|
+
const callID = props.callID;
|
|
1942
|
+
const sessionID = props.sessionID;
|
|
1943
|
+
const error = props.error;
|
|
1944
|
+
if (callID && sessionID && error?.message) {
|
|
1945
|
+
handleToolFailed(callID, sessionID, error.message);
|
|
1737
1946
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1947
|
+
}
|
|
1948
|
+
else if (type === "session.next.tool.success") {
|
|
1949
|
+
const callID = props.callID;
|
|
1950
|
+
if (callID)
|
|
1951
|
+
toolCache.delete(callID);
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
},
|
|
1955
|
+
dispose: async () => {
|
|
1956
|
+
// v1.0.0 (K10-013 / plan §5.3) — the seventh instrumented hook.
|
|
1957
|
+
// HookLiveness.wrap (outside) records the fire only if the
|
|
1958
|
+
// delegate completes; Perf.measureAsync (inside) times it even
|
|
1959
|
+
// on throw.
|
|
1960
|
+
try {
|
|
1961
|
+
await perf.measureAsync("dispose", async () => {
|
|
1747
1962
|
try {
|
|
1748
|
-
|
|
1749
|
-
syncSharedLayer();
|
|
1750
|
-
}
|
|
1963
|
+
await Promise.allSettled([...pending]);
|
|
1751
1964
|
}
|
|
1752
|
-
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1965
|
+
finally {
|
|
1966
|
+
// v1.0.0 (K10-013) — record the fire and persist it now:
|
|
1967
|
+
// this is the last write of the process.
|
|
1968
|
+
liveness.recordDispose();
|
|
1969
|
+
// v1.0.0 (K10-013) — record the successful fire and
|
|
1970
|
+
// disarm any pending deferred-settlement marker: a
|
|
1971
|
+
// clean dispose is exactly what the next session's
|
|
1972
|
+
// settlement check looks for.
|
|
1973
|
+
store
|
|
1974
|
+
.prepare("UPDATE kevin_metrics SET value = value + 1 WHERE key = 'dispose_fires_total'")
|
|
1975
|
+
.run();
|
|
1976
|
+
try {
|
|
1977
|
+
store
|
|
1978
|
+
.prepare("UPDATE kevin_settings SET value = '' WHERE key = 'last_session_recorded_work'")
|
|
1979
|
+
.run();
|
|
1980
|
+
}
|
|
1981
|
+
catch {
|
|
1982
|
+
// best-effort: legacy DBs without kevin_settings
|
|
1983
|
+
}
|
|
1755
1984
|
}
|
|
1756
|
-
}
|
|
1757
|
-
metrics.flush();
|
|
1758
|
-
// v0.9.0 (K9-009 / plan §5.3) — liveness counters persist on
|
|
1759
|
-
// the same cadence as the metrics.
|
|
1760
|
-
liveness.flush();
|
|
1985
|
+
});
|
|
1761
1986
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1987
|
+
finally {
|
|
1988
|
+
// v1.0.0 review fix — persist the final period (including this
|
|
1989
|
+
// dispose sample, recorded by measureAsync above) BEFORE the
|
|
1990
|
+
// closes; otherwise the dispose budget could never be verified
|
|
1991
|
+
// by bench:check because its samples died with the ring.
|
|
1992
|
+
try {
|
|
1993
|
+
perf.flush(store);
|
|
1768
1994
|
}
|
|
1995
|
+
catch {
|
|
1996
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1997
|
+
}
|
|
1998
|
+
metrics.close();
|
|
1999
|
+
store.close();
|
|
1769
2000
|
}
|
|
1770
|
-
else if (type === "session.next.tool.success") {
|
|
1771
|
-
const callID = props.callID;
|
|
1772
|
-
if (callID)
|
|
1773
|
-
toolCache.delete(callID);
|
|
1774
|
-
}
|
|
1775
|
-
},
|
|
1776
|
-
dispose: async () => {
|
|
1777
|
-
await Promise.allSettled([...pending]);
|
|
1778
|
-
liveness.flush();
|
|
1779
|
-
metrics.close();
|
|
1780
|
-
store.close();
|
|
1781
2001
|
},
|
|
1782
2002
|
});
|
|
1783
2003
|
};
|
|
1784
2004
|
export default KevinPlugin;
|
|
1785
|
-
//# sourceMappingURL=index.js.map
|