@jmtrin/opencode-kevin 0.9.0 → 1.0.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 +558 -823
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/plugin/Archiver.js +0 -1
- package/dist/plugin/ArtifactWriter.d.ts +25 -10
- package/dist/plugin/ArtifactWriter.js +43 -25
- package/dist/plugin/CausalChain.js +0 -1
- package/dist/plugin/ConflictDetector.js +0 -1
- 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 +0 -1
- package/dist/plugin/HookLiveness.d.ts +6 -0
- package/dist/plugin/HookLiveness.js +15 -1
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.js +0 -1
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -1
- package/dist/plugin/Migrate.js +25 -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 +7 -1
- package/dist/plugin/SharedLayer.js +0 -1
- package/dist/plugin/Store.js +0 -1
- package/dist/plugin/ToolCallObserver.js +0 -1
- package/dist/plugin/capabilities.js +0 -1
- package/dist/plugin/confidence.js +0 -1
- package/dist/plugin/contract.d.ts +46 -0
- package/dist/plugin/contract.js +368 -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 +425 -227
- 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_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 +0 -1
- package/dist/plugin/memory-format.js +0 -1
- package/dist/plugin/metrics.js +0 -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 +0 -1
- 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/uuid.js +0 -1
- package/package.json +21 -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,7 +29,9 @@ 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";
|
|
35
37
|
import { handleNative } from "./kevin_native.js";
|
|
@@ -40,6 +42,7 @@ import { Metrics } from "./metrics.js";
|
|
|
40
42
|
import { attachNative } from "./native.js";
|
|
41
43
|
import { exportMarkdown, exportOkf } from "./okf-export.js";
|
|
42
44
|
import { importOkf } from "./okf-import.js";
|
|
45
|
+
import { Perf } from "./perf.js";
|
|
43
46
|
import { uuidv7 } from "./uuid.js";
|
|
44
47
|
// v0.6.0 (K6-019) — process-global set of reference topics already
|
|
45
48
|
// registered with the host this process. Keeps re-registration idempotent
|
|
@@ -101,6 +104,13 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
101
104
|
"native_registration_enabled",
|
|
102
105
|
"host_probe_history_enabled",
|
|
103
106
|
"dead_hook_report_threshold",
|
|
107
|
+
// v1.0.0 (K10-005 / plan §6) — the four keys seeded by migration 011
|
|
108
|
+
// section 4. Omitting these makes `kevin_config set` return
|
|
109
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
110
|
+
"perf_enabled",
|
|
111
|
+
"perf_ring_capacity",
|
|
112
|
+
"perf_flush_on_idle",
|
|
113
|
+
"contract_report_enabled",
|
|
104
114
|
];
|
|
105
115
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
106
116
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
@@ -110,7 +120,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
110
120
|
// behaviour on the next reflection.
|
|
111
121
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
112
122
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
113
|
-
export const KEVIN_VERSION = "0.
|
|
123
|
+
export const KEVIN_VERSION = "1.0.0";
|
|
114
124
|
function resolveMigrationsDir() {
|
|
115
125
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
116
126
|
return join(here, "..", "migrations");
|
|
@@ -292,6 +302,15 @@ export const KevinPlugin = async (input, options) => {
|
|
|
292
302
|
thresholdText: memoryService.getSetting("dead_hook_report_threshold", "3"),
|
|
293
303
|
pluginVersion: host.pluginVersion,
|
|
294
304
|
});
|
|
305
|
+
// v1.0.0 (K10-012 / plan §5.2) — the performance instrument. Measures
|
|
306
|
+
// how long each hook holds the host into per-scope ring buffers that
|
|
307
|
+
// only reach the store at session.idle (D10-11: no perf_samples write
|
|
308
|
+
// anywhere else). perf_enabled uses the explicit === "1" TEXT
|
|
309
|
+
// comparison; the capacity string goes through the parse guard/clamp.
|
|
310
|
+
const perf = new Perf({
|
|
311
|
+
enabled: memoryService.getSetting("perf_enabled", "1") === "1",
|
|
312
|
+
capacity: memoryService.getSetting("perf_ring_capacity", "") ?? "",
|
|
313
|
+
});
|
|
295
314
|
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
296
315
|
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
297
316
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
@@ -451,6 +470,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
451
470
|
}
|
|
452
471
|
}
|
|
453
472
|
let currentSessionId = null;
|
|
473
|
+
// v1.0.0 (K10-013 / D10-08) — set on the first tool completion of a
|
|
474
|
+
// session; consumed at idle to arm the deferred-dispose marker.
|
|
475
|
+
let sessionRecordedWork = false;
|
|
454
476
|
// BUG-011 — process-global last derived query. Cleared on
|
|
455
477
|
// `session.idle`; the per-session map below is the preferred source.
|
|
456
478
|
let lastUserQuery = null;
|
|
@@ -1079,6 +1101,49 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1079
1101
|
};
|
|
1080
1102
|
},
|
|
1081
1103
|
}),
|
|
1104
|
+
// v1.0.0 (K10-018 / plan §5.6) — kevin_contract: the frozen
|
|
1105
|
+
// surface, inspectable at runtime rather than only at test
|
|
1106
|
+
// time. Read-only; summary by default, one clause's full value
|
|
1107
|
+
// with clause+format:'full'. Unknown clause ids are a
|
|
1108
|
+
// structured error, never a throw.
|
|
1109
|
+
kevin_contract: tool({
|
|
1110
|
+
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.",
|
|
1111
|
+
args: {
|
|
1112
|
+
clause: tool.schema
|
|
1113
|
+
.string()
|
|
1114
|
+
.optional()
|
|
1115
|
+
.describe("Id de clausula (ej. 'C-01'). Sin clause: resumen."),
|
|
1116
|
+
format: tool.schema
|
|
1117
|
+
.enum(["summary", "full"])
|
|
1118
|
+
.optional()
|
|
1119
|
+
.describe("summary (default): una linea por clausula. full: valores completos (requiere clause para una clausula; solo devuelve resumen enriquecido si se omite)."),
|
|
1120
|
+
},
|
|
1121
|
+
async execute(args) {
|
|
1122
|
+
const result = buildKevinContract({ packageVersion: KEVIN_VERSION }, args);
|
|
1123
|
+
return {
|
|
1124
|
+
title: "Contrato de Kevin",
|
|
1125
|
+
output: JSON.stringify(result),
|
|
1126
|
+
};
|
|
1127
|
+
},
|
|
1128
|
+
}),
|
|
1129
|
+
// v1.0.0 (K10-019 / plan §5.6) — kevin_bench: reports what
|
|
1130
|
+
// `npm run bench` recorded in bench_runs. It NEVER runs the
|
|
1131
|
+
// benchmark from inside a live session.
|
|
1132
|
+
kevin_bench: tool({
|
|
1133
|
+
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.",
|
|
1134
|
+
args: {
|
|
1135
|
+
action: tool.schema
|
|
1136
|
+
.enum(["status", "last"])
|
|
1137
|
+
.describe("status | last"),
|
|
1138
|
+
},
|
|
1139
|
+
async execute(args) {
|
|
1140
|
+
const result = buildKevinBench({ store }, args);
|
|
1141
|
+
return {
|
|
1142
|
+
title: "Benchmark de Kevin",
|
|
1143
|
+
output: JSON.stringify(result),
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
1146
|
+
}),
|
|
1082
1147
|
kevin_retrospective: tool({
|
|
1083
1148
|
description: "Genera un retrospective markdown para una sesion (resume tools que fallaron y lecciones aprendidas).",
|
|
1084
1149
|
args: {
|
|
@@ -1519,267 +1584,400 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1519
1584
|
}),
|
|
1520
1585
|
},
|
|
1521
1586
|
"tool.execute.before": async (hookInput, output) => {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1587
|
+
// v1.0.0 (K10-012) — perf measures the synchronous hold time;
|
|
1588
|
+
// HookLiveness wraps the outside of this handler (compose,
|
|
1589
|
+
// never merge — D10-09).
|
|
1590
|
+
perf.measure("tool.execute.before", () => {
|
|
1591
|
+
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
1592
|
+
observer.onBefore({
|
|
1593
|
+
tool: hookInput.tool,
|
|
1594
|
+
args: output.args,
|
|
1595
|
+
sessionId: hookInput.sessionID,
|
|
1596
|
+
callID: hookInput.callID,
|
|
1597
|
+
projectId,
|
|
1598
|
+
}, {});
|
|
1599
|
+
});
|
|
1530
1600
|
},
|
|
1531
1601
|
"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,
|
|
1602
|
+
perf.measure("tool.execute.after", () => {
|
|
1603
|
+
sessionRecordedWork = true;
|
|
1604
|
+
// v0.9.0 (K9-010 / plan §5.3) — checkpoint: the session reached a
|
|
1605
|
+
// model turn, so the system prompt was assembled and
|
|
1606
|
+
// `experimental.chat.system.transform` MUST have been offered.
|
|
1607
|
+
// Deduped per session inside HookLiveness.expect.
|
|
1608
|
+
if (hookInput.sessionID) {
|
|
1609
|
+
liveness.expect("experimental.chat.system.transform", hookInput.sessionID);
|
|
1610
|
+
}
|
|
1611
|
+
const meta = (output.metadata ?? {});
|
|
1612
|
+
const outputText = String(output.output ?? "");
|
|
1613
|
+
const stderr = String(meta.stderr ?? "");
|
|
1614
|
+
const stdout = String(meta.stdout ?? outputText);
|
|
1615
|
+
const exitCode = pickExitCode(meta);
|
|
1616
|
+
let success;
|
|
1617
|
+
if (meta.success === false) {
|
|
1618
|
+
success = false;
|
|
1619
|
+
}
|
|
1620
|
+
else if (exitCode !== undefined) {
|
|
1621
|
+
success = exitCode === 0;
|
|
1622
|
+
}
|
|
1623
|
+
else if (stderr.length > 0 && ERROR_LINE_RE.test(stderr)) {
|
|
1624
|
+
success = false;
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
const stream = stdout.length > 0 ? stdout : outputText;
|
|
1628
|
+
success = !(stream.length > 0 && STRONG_ERROR_RE.test(stream));
|
|
1629
|
+
}
|
|
1630
|
+
observer.onAfter({
|
|
1631
|
+
tool: hookInput.tool,
|
|
1632
|
+
args: hookInput.args,
|
|
1574
1633
|
sessionId: hookInput.sessionID,
|
|
1575
1634
|
callID: hookInput.callID,
|
|
1576
1635
|
projectId,
|
|
1577
|
-
})
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1636
|
+
}, { success, stdout, stderr, exitCode });
|
|
1637
|
+
if (!success) {
|
|
1638
|
+
const errorType = observer.inferErrorType(stderr, stdout, exitCode);
|
|
1639
|
+
fireAndForget(reflector.invoke({
|
|
1640
|
+
toolName: hookInput.tool,
|
|
1641
|
+
argsSummary: observer.summarizeArgs(hookInput.args),
|
|
1642
|
+
stderr,
|
|
1643
|
+
stdout,
|
|
1644
|
+
exitCode,
|
|
1645
|
+
errorType,
|
|
1646
|
+
sessionId: hookInput.sessionID,
|
|
1647
|
+
callID: hookInput.callID,
|
|
1648
|
+
projectId,
|
|
1649
|
+
}));
|
|
1650
|
+
}
|
|
1651
|
+
else {
|
|
1652
|
+
causalChain.onSuccess(hookInput.tool, hookInput.args, projectId, hookInput.sessionID);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1582
1655
|
},
|
|
1583
1656
|
"chat.message": async (hookInput, output) => {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1657
|
+
perf.measure("chat.message", () => {
|
|
1658
|
+
const text = output.parts
|
|
1659
|
+
.map((p) => p)
|
|
1660
|
+
.filter((p) => p.type === "text")
|
|
1661
|
+
.map((p) => p.text ?? "")
|
|
1662
|
+
.join(" ");
|
|
1663
|
+
if (text.trim()) {
|
|
1664
|
+
const derived = injector.deriveQuery([
|
|
1665
|
+
{ role: "user", content: text },
|
|
1666
|
+
]);
|
|
1667
|
+
lastUserQuery = derived.length > 0 ? derived : null;
|
|
1668
|
+
if (hookInput.sessionID && lastUserQuery) {
|
|
1669
|
+
lastUserQueryBySession.set(hookInput.sessionID, lastUserQuery);
|
|
1670
|
+
}
|
|
1594
1671
|
}
|
|
1595
|
-
}
|
|
1672
|
+
});
|
|
1596
1673
|
},
|
|
1597
1674
|
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1675
|
+
perf.measure("chat.system.transform", () => {
|
|
1676
|
+
// BUG-011 — prefer the per-session query so a new session whose
|
|
1677
|
+
// first transform fires before any chat.message cannot reuse the
|
|
1678
|
+
// previous session's query (the global is cleared on idle).
|
|
1679
|
+
const query = lastUserQueryBySession.get(hookInput.sessionID ?? "") ??
|
|
1680
|
+
lastUserQuery;
|
|
1681
|
+
if (!query)
|
|
1682
|
+
return;
|
|
1683
|
+
const suggestion = injector.generateSuggestion();
|
|
1684
|
+
if (suggestion)
|
|
1685
|
+
output.system.push(suggestion);
|
|
1686
|
+
injector.onSystemTransform({
|
|
1687
|
+
sessionID: hookInput.sessionID ?? undefined,
|
|
1688
|
+
messages: [{ role: "user", content: query }],
|
|
1689
|
+
}, output);
|
|
1690
|
+
});
|
|
1611
1691
|
},
|
|
1612
1692
|
"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
|
-
|
|
1693
|
+
perf.measure("session.compacting", () => {
|
|
1694
|
+
// v0.4.0 (K4-018) — plan §5.6: compaction often fires with no
|
|
1695
|
+
// recent chat.message (auto-compact after a long tool turn,
|
|
1696
|
+
// resumed sessions). Resolve a query per session first, then
|
|
1697
|
+
// the global fallback, then any messages the runtime may
|
|
1698
|
+
// provide (defensive — the current SDK contract only exposes
|
|
1699
|
+
// sessionID).
|
|
1700
|
+
// BUG-012 — the HITL suggestion fires AT MOST ONCE per session:
|
|
1701
|
+
// whichever hook (transform or compacting) runs first consumes
|
|
1702
|
+
// the pending recurrence signal (generateSuggestion resets it).
|
|
1703
|
+
const suggestion = injector.generateSuggestion();
|
|
1704
|
+
if (suggestion)
|
|
1705
|
+
output.context.push(suggestion);
|
|
1706
|
+
const sid = hookInput.sessionID;
|
|
1707
|
+
const sessionQuery = lastUserQueryBySession.get(sid) ?? lastUserQuery;
|
|
1708
|
+
const runtimeMessages = hookInput
|
|
1709
|
+
.messages;
|
|
1710
|
+
const messages = sessionQuery != null
|
|
1711
|
+
? [{ role: "user", content: sessionQuery }]
|
|
1712
|
+
: (runtimeMessages ?? []);
|
|
1713
|
+
injector.onCompacting({
|
|
1714
|
+
sessionID: sid,
|
|
1715
|
+
messages,
|
|
1716
|
+
}, output);
|
|
1717
|
+
});
|
|
1636
1718
|
},
|
|
1637
1719
|
event: async ({ event }) => {
|
|
1638
1720
|
const type = event.type;
|
|
1639
1721
|
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") {
|
|
1722
|
+
if (type === "session.idle") {
|
|
1723
|
+
// v1.0.0 (K10-012) — the idle branch measures under
|
|
1724
|
+
// "session.idle" (150/600 ms budget); every other branch
|
|
1725
|
+
// measures under "event" (5/25 ms). Recording idle's ~150 ms
|
|
1726
|
+
// under "event" would make that budget permanently breached
|
|
1727
|
+
// and therefore ignored.
|
|
1654
1728
|
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
|
-
|
|
1729
|
+
await perf.measureAsync("session.idle", async () => {
|
|
1730
|
+
if (sid) {
|
|
1731
|
+
toolCache.clear();
|
|
1732
|
+
// BUG-011 — the session is done: drop the global query
|
|
1733
|
+
// so the next session cannot reuse it.
|
|
1734
|
+
lastUserQuery = null;
|
|
1735
|
+
// v0.4.0 (K4-024) — plan §5.2: settle the session's
|
|
1736
|
+
// unmeasured injections (effective/ineffective +
|
|
1737
|
+
// recurrence_count charges) at idle. Best-effort: a
|
|
1738
|
+
// legacy DB without migration 005 has no ledger table.
|
|
1739
|
+
try {
|
|
1740
|
+
ledger.settle(sid);
|
|
1741
|
+
}
|
|
1742
|
+
catch {
|
|
1743
|
+
// best-effort: a legacy DB without the ledger
|
|
1744
|
+
// table must not break the idle path
|
|
1745
|
+
}
|
|
1746
|
+
// v0.5.0 (K5-012 / plan §5.4) — retire stale lessons at
|
|
1747
|
+
// idle; pre-006 DBs degrade to a no-op.
|
|
1748
|
+
try {
|
|
1749
|
+
archiver.run();
|
|
1750
|
+
}
|
|
1751
|
+
catch {
|
|
1752
|
+
// best-effort, same pattern as ledger.settle
|
|
1753
|
+
}
|
|
1754
|
+
fireAndForget(retrospective.generate(sid));
|
|
1755
|
+
memoryService.boostPositiveReflectors(sid);
|
|
1756
|
+
const recurred = memoryService.penalizeRecurringReflectors(sid);
|
|
1757
|
+
injector.setRecurrences(recurred, sid);
|
|
1758
|
+
patternMiner.mine(projectId);
|
|
1759
|
+
// v0.7.0 (K7-012 / plan §5.4, D7-10) — convention mining is
|
|
1760
|
+
// session.idle-only, default-off, and isolated from the rest of
|
|
1761
|
+
// the idle chain. Mined rules still enter the ordinary Curator
|
|
1762
|
+
// approval path; this step never writes to the repository.
|
|
1763
|
+
try {
|
|
1764
|
+
if (memoryService.getSetting("convention_mining_enabled", "0") ===
|
|
1765
|
+
"1") {
|
|
1766
|
+
const conventions = conventionMiner.mine();
|
|
1767
|
+
conventionMiner.emit(conventions);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
// A throwing miner must not reject or truncate the idle chain.
|
|
1772
|
+
}
|
|
1773
|
+
// v0.7.0 (K7-016 / plan §5.5, D7-06) — conflict detection is
|
|
1774
|
+
// surfacing-only on idle. It may create/open conflict rows, but
|
|
1775
|
+
// no idle path can acknowledge or resolve one.
|
|
1776
|
+
try {
|
|
1777
|
+
if (memoryService.getSetting("conflict_detection_enabled", "0") ===
|
|
1778
|
+
"1") {
|
|
1779
|
+
conflictDetector.detect();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
catch {
|
|
1783
|
+
// Conflict surfacing is best-effort and must not reject idle.
|
|
1784
|
+
}
|
|
1785
|
+
fireAndForget(Promise.resolve()
|
|
1786
|
+
.then(() => causalChain.onSessionIdle(sid))
|
|
1787
|
+
// non-blocking — promote is a best-effort pass
|
|
1788
|
+
// (legacy DBs pre-005 lack the recurrence_count
|
|
1789
|
+
// column)
|
|
1790
|
+
.catch(() => { }));
|
|
1791
|
+
// v0.6.0 (K6-015 / plan §8.14) — session-idle curation
|
|
1792
|
+
// generation. Dry-run only: `propose()` calls `plan()`
|
|
1793
|
+
// and never `apply()`, so nothing here can touch disk
|
|
1794
|
+
// (D6-01). Guarded by curation_enabled (TEXT compare)
|
|
1795
|
+
// and a 1-hour throttle persisted in kevin_settings.
|
|
1796
|
+
try {
|
|
1797
|
+
if (memoryService.getSetting("curation_enabled", "1") === "1") {
|
|
1798
|
+
const CURATION_THROTTLE_MS = 3_600_000;
|
|
1799
|
+
const last = memoryService.getSetting("last_curation_at", "");
|
|
1800
|
+
if (last === "" ||
|
|
1801
|
+
Date.now() - Date.parse(last) > CURATION_THROTTLE_MS) {
|
|
1802
|
+
curator.propose("agents_md", writer);
|
|
1803
|
+
store
|
|
1804
|
+
.prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
|
|
1805
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
1806
|
+
.run("last_curation_at", new Date().toISOString());
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
catch {
|
|
1811
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1812
|
+
// curation failure must not break the idle path
|
|
1813
|
+
}
|
|
1814
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer
|
|
1815
|
+
// re-read at idle. Gated by shared_layer_enabled ===
|
|
1816
|
+
// "1", a TEXT comparison: '0' is a truthy string, so a
|
|
1817
|
+
// truthiness check would turn the release on for every
|
|
1818
|
+
// installation that upgrades. Never wired into
|
|
1819
|
+
// tool.execute.*, chat.message, system.transform or
|
|
1820
|
+
// session.compacting — the hot-path rule is absolute;
|
|
1821
|
+
// the file-hash skip is what makes the idle cost one
|
|
1822
|
+
// read plus one hash on an unchanged repository.
|
|
1823
|
+
try {
|
|
1824
|
+
if (memoryService.getSetting("shared_layer_enabled", "0") === "1") {
|
|
1825
|
+
syncSharedLayer();
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch {
|
|
1829
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1830
|
+
// sync failure must not break the idle path
|
|
1831
|
+
}
|
|
1832
|
+
// v1.0.0 (K10-013 / D10-08) — the session recorded work:
|
|
1833
|
+
// arm the deferred dispose settlement. The ISO timestamp
|
|
1834
|
+
// lets the next session.start compare it against
|
|
1835
|
+
// hook_liveness.last_seen_at for dispose.
|
|
1836
|
+
if (sessionRecordedWork) {
|
|
1837
|
+
try {
|
|
1838
|
+
store
|
|
1839
|
+
.prepare("INSERT INTO kevin_settings (key, value) VALUES ('last_session_recorded_work', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
1840
|
+
.run(new Date().toISOString());
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
// best-effort: arming must not break idle
|
|
1844
|
+
}
|
|
1845
|
+
sessionRecordedWork = false;
|
|
1692
1846
|
}
|
|
1693
1847
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1848
|
+
});
|
|
1849
|
+
metrics.flush();
|
|
1850
|
+
// v1.0.0 (K10-012 / plan §5.2, D10-11) — the idle perf_samples
|
|
1851
|
+
// write (the dispose hook holds the other one), gated by
|
|
1852
|
+
// perf_flush_on_idle (TEXT compare; the fallback matches the
|
|
1853
|
+
// migration-seeded default '1').
|
|
1854
|
+
if (memoryService.getSetting("perf_flush_on_idle", "1") === "1") {
|
|
1700
1855
|
try {
|
|
1701
|
-
|
|
1702
|
-
"1") {
|
|
1703
|
-
conflictDetector.detect();
|
|
1704
|
-
}
|
|
1856
|
+
perf.flush(store);
|
|
1705
1857
|
}
|
|
1706
1858
|
catch {
|
|
1707
|
-
//
|
|
1859
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1708
1860
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1861
|
+
}
|
|
1862
|
+
liveness.flush();
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
perf.measure("event", () => {
|
|
1866
|
+
if (type === "session.created") {
|
|
1867
|
+
const info = props.info;
|
|
1868
|
+
if (info?.id) {
|
|
1869
|
+
currentSessionId = info.id;
|
|
1870
|
+
// BUG-011 — a fresh session must not inherit the
|
|
1871
|
+
// previous session's derived query (the global may
|
|
1872
|
+
// still hold it if no idle fired).
|
|
1873
|
+
lastUserQueryBySession.delete(info.id);
|
|
1874
|
+
// v0.4.0 (K4-017) — plan §5.1 rule 3: the per-session
|
|
1875
|
+
// seen-set resets when a session is created.
|
|
1876
|
+
injector.onSessionCreated(info.id);
|
|
1877
|
+
// v1.0.0 (K10-013 / plan §5.3, D10-08) — deferred
|
|
1878
|
+
// dispose settlement. The previous session recorded
|
|
1879
|
+
// work but the process never came back through
|
|
1880
|
+
// `dispose` (crash or hard kill): settle it HERE,
|
|
1881
|
+
// at the start of the next session, because dispose
|
|
1882
|
+
// cannot be settled within the session that observes
|
|
1883
|
+
// it. One settlement per work marker; a first-ever
|
|
1884
|
+
// run has no marker and never reports dead. The
|
|
1885
|
+
// threshold semantics stay with expect(): unknown
|
|
1886
|
+
// until expected_count crosses the threshold.
|
|
1887
|
+
try {
|
|
1888
|
+
const marker = memoryService.getSetting("last_session_recorded_work", "");
|
|
1889
|
+
if (marker !== "") {
|
|
1890
|
+
let fired = true;
|
|
1891
|
+
try {
|
|
1892
|
+
const row = store
|
|
1893
|
+
.prepare("SELECT last_seen_at FROM hook_liveness WHERE hook = 'dispose'")
|
|
1894
|
+
.get();
|
|
1895
|
+
const last = row?.last_seen_at ?? null;
|
|
1896
|
+
fired = last !== null && last > marker;
|
|
1897
|
+
}
|
|
1898
|
+
catch {
|
|
1899
|
+
// pre-010 DB without hook_liveness: treat as
|
|
1900
|
+
// no evidence of a fire, same as a crash.
|
|
1901
|
+
fired = false;
|
|
1902
|
+
}
|
|
1903
|
+
if (!fired) {
|
|
1904
|
+
liveness.expect("dispose", `miss:${marker}`);
|
|
1905
|
+
store
|
|
1906
|
+
.prepare("UPDATE kevin_metrics SET value = value + 1 WHERE key = 'dispose_misses_total'")
|
|
1907
|
+
.run();
|
|
1908
|
+
}
|
|
1727
1909
|
store
|
|
1728
|
-
.prepare(
|
|
1729
|
-
|
|
1730
|
-
.run("last_curation_at", new Date().toISOString());
|
|
1910
|
+
.prepare("UPDATE kevin_settings SET value = '' WHERE key = 'last_session_recorded_work'")
|
|
1911
|
+
.run();
|
|
1731
1912
|
}
|
|
1732
1913
|
}
|
|
1914
|
+
catch {
|
|
1915
|
+
// best-effort: settlement must not break session start
|
|
1916
|
+
}
|
|
1733
1917
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1918
|
+
}
|
|
1919
|
+
else if (type === "session.next.tool.failed") {
|
|
1920
|
+
const callID = props.callID;
|
|
1921
|
+
const sessionID = props.sessionID;
|
|
1922
|
+
const error = props.error;
|
|
1923
|
+
if (callID && sessionID && error?.message) {
|
|
1924
|
+
handleToolFailed(callID, sessionID, error.message);
|
|
1737
1925
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1926
|
+
}
|
|
1927
|
+
else if (type === "session.next.tool.success") {
|
|
1928
|
+
const callID = props.callID;
|
|
1929
|
+
if (callID)
|
|
1930
|
+
toolCache.delete(callID);
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
},
|
|
1934
|
+
dispose: async () => {
|
|
1935
|
+
// v1.0.0 (K10-013 / plan §5.3) — the seventh instrumented hook.
|
|
1936
|
+
// HookLiveness.wrap (outside) records the fire only if the
|
|
1937
|
+
// delegate completes; Perf.measureAsync (inside) times it even
|
|
1938
|
+
// on throw.
|
|
1939
|
+
try {
|
|
1940
|
+
await perf.measureAsync("dispose", async () => {
|
|
1747
1941
|
try {
|
|
1748
|
-
|
|
1749
|
-
syncSharedLayer();
|
|
1750
|
-
}
|
|
1942
|
+
await Promise.allSettled([...pending]);
|
|
1751
1943
|
}
|
|
1752
|
-
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1944
|
+
finally {
|
|
1945
|
+
// v1.0.0 (K10-013) — record the fire and persist it now:
|
|
1946
|
+
// this is the last write of the process.
|
|
1947
|
+
liveness.recordDispose();
|
|
1948
|
+
// v1.0.0 (K10-013) — record the successful fire and
|
|
1949
|
+
// disarm any pending deferred-settlement marker: a
|
|
1950
|
+
// clean dispose is exactly what the next session's
|
|
1951
|
+
// settlement check looks for.
|
|
1952
|
+
store
|
|
1953
|
+
.prepare("UPDATE kevin_metrics SET value = value + 1 WHERE key = 'dispose_fires_total'")
|
|
1954
|
+
.run();
|
|
1955
|
+
try {
|
|
1956
|
+
store
|
|
1957
|
+
.prepare("UPDATE kevin_settings SET value = '' WHERE key = 'last_session_recorded_work'")
|
|
1958
|
+
.run();
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
// best-effort: legacy DBs without kevin_settings
|
|
1962
|
+
}
|
|
1755
1963
|
}
|
|
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();
|
|
1964
|
+
});
|
|
1761
1965
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1966
|
+
finally {
|
|
1967
|
+
// v1.0.0 review fix — persist the final period (including this
|
|
1968
|
+
// dispose sample, recorded by measureAsync above) BEFORE the
|
|
1969
|
+
// closes; otherwise the dispose budget could never be verified
|
|
1970
|
+
// by bench:check because its samples died with the ring.
|
|
1971
|
+
try {
|
|
1972
|
+
perf.flush(store);
|
|
1768
1973
|
}
|
|
1974
|
+
catch {
|
|
1975
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1976
|
+
}
|
|
1977
|
+
metrics.close();
|
|
1978
|
+
store.close();
|
|
1769
1979
|
}
|
|
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
1980
|
},
|
|
1782
1981
|
});
|
|
1783
1982
|
};
|
|
1784
1983
|
export default KevinPlugin;
|
|
1785
|
-
//# sourceMappingURL=index.js.map
|