@jmtrin/opencode-kevin 0.8.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 +559 -771
- package/dist/migrations/010_v09_native.sql +78 -0
- 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 +87 -0
- package/dist/plugin/HookLiveness.js +338 -0
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.d.ts +25 -0
- package/dist/plugin/Materializer.js +39 -11
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +69 -6
- 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.d.ts +20 -9
- package/dist/plugin/RepoIdentity.js +44 -10
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +16 -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.d.ts +70 -0
- package/dist/plugin/host.js +250 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +542 -222
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +60 -0
- package/dist/plugin/kevin_audit.js +144 -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 +62 -0
- package/dist/plugin/kevin_doctor.js +196 -0
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +79 -0
- 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.d.ts +92 -0
- package/dist/plugin/native.js +190 -0
- 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 +22 -7
- 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/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/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_facts.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/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/dist/plugin/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ContextInjector } from "./ContextInjector.js";
|
|
|
11
11
|
import { ConventionMiner } from "./ConventionMiner.js";
|
|
12
12
|
import { Curator } from "./Curator.js";
|
|
13
13
|
import { Feedback } from "./Feedback.js";
|
|
14
|
+
import { HookLiveness } from "./HookLiveness.js";
|
|
14
15
|
import { InjectionLedger } from "./InjectionLedger.js";
|
|
15
16
|
import { Materializer, SKILL_TOPIC } from "./Materializer.js";
|
|
16
17
|
import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
|
|
@@ -25,16 +26,24 @@ import { Store } from "./Store.js";
|
|
|
25
26
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
26
27
|
import { probe } from "./capabilities.js";
|
|
27
28
|
import { computeConfidence } from "./confidence.js";
|
|
29
|
+
import { probeHost, summarize } from "./host.js";
|
|
28
30
|
import { kevinApprove } from "./kevin_approve.js";
|
|
29
31
|
import { buildAudit } from "./kevin_audit.js";
|
|
32
|
+
import { buildKevinBench } from "./kevin_bench.js";
|
|
30
33
|
import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
34
|
+
import { buildKevinContract } from "./kevin_contract.js";
|
|
35
|
+
import { buildDoctor } from "./kevin_doctor.js";
|
|
31
36
|
import { buildKevinFacts } from "./kevin_facts.js";
|
|
37
|
+
import { handleNative } from "./kevin_native.js";
|
|
32
38
|
import { kevinPropose } from "./kevin_propose.js";
|
|
33
39
|
import { kevinPublish } from "./kevin_publish.js";
|
|
34
40
|
import { kevinWhy } from "./kevin_why.js";
|
|
35
41
|
import { Metrics } from "./metrics.js";
|
|
42
|
+
import { attachNative } from "./native.js";
|
|
36
43
|
import { exportMarkdown, exportOkf } from "./okf-export.js";
|
|
37
44
|
import { importOkf } from "./okf-import.js";
|
|
45
|
+
import { Perf } from "./perf.js";
|
|
46
|
+
import { uuidv7 } from "./uuid.js";
|
|
38
47
|
// v0.6.0 (K6-019) — process-global set of reference topics already
|
|
39
48
|
// registered with the host this process. Keeps re-registration idempotent
|
|
40
49
|
// across sessions within one process: a topic registered once is never
|
|
@@ -84,6 +93,24 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
84
93
|
"share_requires_approval",
|
|
85
94
|
"author_identity_mode",
|
|
86
95
|
"shared_confidence_floor",
|
|
96
|
+
// v0.9.0 (K9-003 / plan §8.10) — the four keys seeded by migration 010
|
|
97
|
+
// section 5. Omitting these makes `kevin_config set` return
|
|
98
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
99
|
+
// hook_liveness_enabled and the two registration/history flags must be
|
|
100
|
+
// compared with === "1" (they are TEXT, and '0' is truthy);
|
|
101
|
+
// dead_hook_report_threshold is a string read with Number.parseInt and
|
|
102
|
+
// clamped to [1, 1000], NaN defaulting to 3 (conventions, §2; D9-09).
|
|
103
|
+
"hook_liveness_enabled",
|
|
104
|
+
"native_registration_enabled",
|
|
105
|
+
"host_probe_history_enabled",
|
|
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",
|
|
87
114
|
];
|
|
88
115
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
89
116
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
@@ -93,7 +120,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
93
120
|
// behaviour on the next reflection.
|
|
94
121
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
95
122
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
96
|
-
export const KEVIN_VERSION = "0.
|
|
123
|
+
export const KEVIN_VERSION = "1.0.0";
|
|
97
124
|
function resolveMigrationsDir() {
|
|
98
125
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
99
126
|
return join(here, "..", "migrations");
|
|
@@ -235,7 +262,12 @@ export const KevinPlugin = async (input, options) => {
|
|
|
235
262
|
// (plan §5.7 fallback; D2-11 project scoping wired into the live path).
|
|
236
263
|
// v0.8.0 (K8-006 / plan §5.1): the repository identity resolves once at
|
|
237
264
|
// init, next to the project id, and never on a hot path.
|
|
238
|
-
|
|
265
|
+
// v0.9.0 (K9-004/K9-006 / plan §5.1-5.2, D9-12/D9-13): the host surface
|
|
266
|
+
// is probed once at construction — frozen, never re-probed — and feeds
|
|
267
|
+
// the identity chain as the third source, above `path` and below
|
|
268
|
+
// `declared`/`remote`.
|
|
269
|
+
const host = await probeHost(input);
|
|
270
|
+
const identity = RepoIdentity.resolve(process.cwd(), host);
|
|
239
271
|
const projectId = identity.projectId;
|
|
240
272
|
const repoId = identity.repoId;
|
|
241
273
|
const identitySource = identity.source;
|
|
@@ -249,6 +281,36 @@ export const KevinPlugin = async (input, options) => {
|
|
|
249
281
|
// not what the session is scoped on.
|
|
250
282
|
let sessionIdentity = identity;
|
|
251
283
|
const memoryService = new MemoryService(store, metrics, repoId);
|
|
284
|
+
// v0.9.0 (K9-008 / plan §5.1, D9-08) — host probe history: one
|
|
285
|
+
// host_probes row per construction when host_probe_history_enabled
|
|
286
|
+
// is explicitly '1' (TEXT comparison — truthiness would fire on
|
|
287
|
+
// '0'). Append-only and unbounded by design: off by default, one
|
|
288
|
+
// row per process start, no retention policy.
|
|
289
|
+
if (memoryService.getSetting("host_probe_history_enabled", "0") === "1") {
|
|
290
|
+
store
|
|
291
|
+
.prepare(`INSERT INTO host_probes
|
|
292
|
+
(id, plugin_version, flavour, has_shell, v2_skill, v2_reference, notes)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
294
|
+
.run(uuidv7(), host.pluginVersion, host.flavour, host.hasShell ? 1 : 0, host.v2.skill ? 1 : 0, host.v2.reference ? 1 : 0, host.notes.join("\n"));
|
|
295
|
+
}
|
|
296
|
+
// v0.9.0 (K9-009 / plan §5.3, D9-07/D9-08) — hook liveness. Wraps every
|
|
297
|
+
// hook with a success-path recorder; counters persist on the
|
|
298
|
+
// metrics.flush() cadence. Explicit === "1" TEXT comparison
|
|
299
|
+
// (kevin_settings.value is TEXT).
|
|
300
|
+
const liveness = new HookLiveness(store, {
|
|
301
|
+
enabled: memoryService.getSetting("hook_liveness_enabled", "1") === "1",
|
|
302
|
+
thresholdText: memoryService.getSetting("dead_hook_report_threshold", "3"),
|
|
303
|
+
pluginVersion: host.pluginVersion,
|
|
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
|
+
});
|
|
252
314
|
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
253
315
|
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
254
316
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
@@ -334,6 +396,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
334
396
|
mkdirSync(join(materializerRoot, "refs"), { recursive: true });
|
|
335
397
|
const capabilities = probe(input);
|
|
336
398
|
const materializer = new Materializer(store, { root: materializerRoot });
|
|
399
|
+
// v0.9.0 (K9-016 / plan §5.4, D9-10) — native registration replaces
|
|
400
|
+
// file emission. When attachNative returns a registration for a
|
|
401
|
+
// surface, the corresponding *_emission_enabled path is skipped for
|
|
402
|
+
// that surface only. The guard lives in Materializer.
|
|
403
|
+
try {
|
|
404
|
+
const nativeReg = await attachNative(host, {
|
|
405
|
+
materializer,
|
|
406
|
+
settings: memoryService,
|
|
407
|
+
store,
|
|
408
|
+
});
|
|
409
|
+
if (nativeReg) {
|
|
410
|
+
materializer.markNativeRegistered("skill", nativeReg.registered.skill);
|
|
411
|
+
materializer.markNativeRegistered("reference", nativeReg.registered.reference);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// attachNative never throws (D9-12) — guard against unexpected rejection.
|
|
416
|
+
}
|
|
337
417
|
// v0.6.0 (K6-018 / plan §8.14, D6-13) — skill emission at session
|
|
338
418
|
// start, behind the capability probe AND `skill_emission_enabled`
|
|
339
419
|
// (default '0'). Both no-op paths are silent: "unavailable" (v1 host)
|
|
@@ -390,6 +470,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
390
470
|
}
|
|
391
471
|
}
|
|
392
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;
|
|
393
476
|
// BUG-011 — process-global last derived query. Cleared on
|
|
394
477
|
// `session.idle`; the per-session map below is the preferred source.
|
|
395
478
|
let lastUserQuery = null;
|
|
@@ -445,7 +528,11 @@ export const KevinPlugin = async (input, options) => {
|
|
|
445
528
|
projectId,
|
|
446
529
|
}));
|
|
447
530
|
}
|
|
448
|
-
|
|
531
|
+
// v0.9.0 (K9-009 / plan §5.3) — the hooks object is wrapped here at the
|
|
532
|
+
// point of return: transparent (identical keys/arity/returns/errors),
|
|
533
|
+
// recording fires on the success path only. With hook_liveness_enabled
|
|
534
|
+
// '0' the wrapper returns this same object untouched.
|
|
535
|
+
return liveness.wrap({
|
|
449
536
|
tool: {
|
|
450
537
|
kevin_save: tool({
|
|
451
538
|
description: "Guarda una memoria en el conocimiento persistente de Kevin (error, pattern, decision o context).",
|
|
@@ -783,8 +870,8 @@ export const KevinPlugin = async (input, options) => {
|
|
|
783
870
|
// monotone across releases; K6-024 extends this
|
|
784
871
|
// block with the remaining v0.6 fields. v0.8.0
|
|
785
872
|
// (K8-025) — 18 v0.7 + kevin_project +
|
|
786
|
-
//
|
|
787
|
-
tool_count:
|
|
873
|
+
// kevin_native = 23.
|
|
874
|
+
tool_count: 23,
|
|
788
875
|
v07,
|
|
789
876
|
memories_reflector: memoriesReflector,
|
|
790
877
|
memories_agent: memoriesAgent,
|
|
@@ -819,6 +906,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
819
906
|
v06,
|
|
820
907
|
// v0.8.0 (K8-025) — omitted on pre-009 DBs.
|
|
821
908
|
v08,
|
|
909
|
+
// v0.9.0 (K9-008 / plan §5.1) — one-line host
|
|
910
|
+
// summary: version, flavour, shell, v2 flags;
|
|
911
|
+
// no paths, no ids (charset-safe for log lines).
|
|
912
|
+
host_summary: summarize(host),
|
|
822
913
|
}),
|
|
823
914
|
};
|
|
824
915
|
},
|
|
@@ -846,7 +937,7 @@ export const KevinPlugin = async (input, options) => {
|
|
|
846
937
|
path: res.path,
|
|
847
938
|
id: res.id,
|
|
848
939
|
created_at: res.createdAt,
|
|
849
|
-
generator: "opencode-kevin/0.
|
|
940
|
+
generator: "opencode-kevin/0.9.0",
|
|
850
941
|
}
|
|
851
942
|
: {
|
|
852
943
|
action: "init",
|
|
@@ -968,6 +1059,91 @@ export const KevinPlugin = async (input, options) => {
|
|
|
968
1059
|
};
|
|
969
1060
|
},
|
|
970
1061
|
}),
|
|
1062
|
+
// v0.9.0 (K9-018 / plan §5.5, D9-09) — kevin_doctor: pure
|
|
1063
|
+
// reads, no writes, no probe re-run, no model call. The host
|
|
1064
|
+
// block comes from the frozen init-time probe, hooks from the
|
|
1065
|
+
// persisted hook_liveness table, native from the last
|
|
1066
|
+
// registration per surface, verdict from reduceVerdict.
|
|
1067
|
+
kevin_doctor: tool({
|
|
1068
|
+
description: "Doctor de Kevin (v0.9.0): salud del host, hooks, dependencias y registros nativos. Solo lectura, sin LLM ni writes, invocable en cualquier momento. Devuelve host (version, flavour, shell, v2), hooks ordenados dead primero, dependencies (zod_copies), native (enabled, registered/verified por surface), verdict (healthy|degraded|unknown) y reason. Output pensado para pegar en un issue: sin filesystem paths ni session ids.",
|
|
1069
|
+
args: {},
|
|
1070
|
+
async execute() {
|
|
1071
|
+
const report = buildDoctor(store, host, memoryService);
|
|
1072
|
+
return {
|
|
1073
|
+
title: "Doctor de Kevin",
|
|
1074
|
+
output: JSON.stringify(report),
|
|
1075
|
+
};
|
|
1076
|
+
},
|
|
1077
|
+
}),
|
|
1078
|
+
// v0.9.0 (K9-019 / plan §5.5, D9-12) — kevin_native: inspect
|
|
1079
|
+
// and toggle native_registration_enabled. enable/disable write
|
|
1080
|
+
// kevin_settings only and never re-attach — the probe is
|
|
1081
|
+
// frozen for the process lifetime, so a restart is the
|
|
1082
|
+
// requirement for the change to take effect. `enable` on a
|
|
1083
|
+
// host without the v2 subpath succeeds and reports the
|
|
1084
|
+
// registration as inert: the setting is a statement of intent.
|
|
1085
|
+
kevin_native: tool({
|
|
1086
|
+
description: "Registros nativos v2 (v0.9.0): show reporta el setting native_registration_enabled, si el host resuelto expone el subpath v2 (effective) y las ultimas filas de native_registrations por surface; enable/disable escriben el setting ('1'/'0' TEXT) y NO re-adjuntan — el probe es frozen para la vida del proceso, un restart del host es el requisito para que el cambio tenga efecto; enable en un host sin subpath v2 exito con registration inert.",
|
|
1087
|
+
args: {
|
|
1088
|
+
action: tool.schema
|
|
1089
|
+
.enum(["show", "enable", "disable"])
|
|
1090
|
+
.default("show"),
|
|
1091
|
+
},
|
|
1092
|
+
async execute(args) {
|
|
1093
|
+
const report = handleNative(args.action, {
|
|
1094
|
+
host,
|
|
1095
|
+
store,
|
|
1096
|
+
settings: memoryService,
|
|
1097
|
+
});
|
|
1098
|
+
return {
|
|
1099
|
+
title: "Kevin native",
|
|
1100
|
+
output: JSON.stringify(report),
|
|
1101
|
+
};
|
|
1102
|
+
},
|
|
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
|
+
}),
|
|
971
1147
|
kevin_retrospective: tool({
|
|
972
1148
|
description: "Genera un retrospective markdown para una sesion (resume tools que fallaron y lecciones aprendidas).",
|
|
973
1149
|
args: {
|
|
@@ -1408,256 +1584,400 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1408
1584
|
}),
|
|
1409
1585
|
},
|
|
1410
1586
|
"tool.execute.before": async (hookInput, output) => {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
+
});
|
|
1419
1600
|
},
|
|
1420
1601
|
"tool.execute.after": async (hookInput, output) => {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
stderr,
|
|
1453
|
-
stdout,
|
|
1454
|
-
exitCode,
|
|
1455
|
-
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,
|
|
1456
1633
|
sessionId: hookInput.sessionID,
|
|
1457
1634
|
callID: hookInput.callID,
|
|
1458
1635
|
projectId,
|
|
1459
|
-
})
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
+
});
|
|
1464
1655
|
},
|
|
1465
1656
|
"chat.message": async (hookInput, output) => {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
+
}
|
|
1476
1671
|
}
|
|
1477
|
-
}
|
|
1672
|
+
});
|
|
1478
1673
|
},
|
|
1479
1674
|
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
});
|
|
1493
1691
|
},
|
|
1494
1692
|
"experimental.session.compacting": async (hookInput, output) => {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
+
});
|
|
1518
1718
|
},
|
|
1519
1719
|
event: async ({ event }) => {
|
|
1520
1720
|
const type = event.type;
|
|
1521
1721
|
const props = event.properties ?? {};
|
|
1522
|
-
if (type === "session.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
// still hold it if no idle fired).
|
|
1529
|
-
lastUserQueryBySession.delete(info.id);
|
|
1530
|
-
// v0.4.0 (K4-017) — plan §5.1 rule 3: the per-session
|
|
1531
|
-
// seen-set resets when a session is created.
|
|
1532
|
-
injector.onSessionCreated(info.id);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
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.
|
|
1536
1728
|
const sid = props.sessionID;
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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;
|
|
1574
1846
|
}
|
|
1575
1847
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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") {
|
|
1582
1855
|
try {
|
|
1583
|
-
|
|
1584
|
-
"1") {
|
|
1585
|
-
conflictDetector.detect();
|
|
1586
|
-
}
|
|
1856
|
+
perf.flush(store);
|
|
1587
1857
|
}
|
|
1588
1858
|
catch {
|
|
1589
|
-
//
|
|
1859
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1590
1860
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
+
}
|
|
1609
1909
|
store
|
|
1610
|
-
.prepare(
|
|
1611
|
-
|
|
1612
|
-
.run("last_curation_at", new Date().toISOString());
|
|
1910
|
+
.prepare("UPDATE kevin_settings SET value = '' WHERE key = 'last_session_recorded_work'")
|
|
1911
|
+
.run();
|
|
1613
1912
|
}
|
|
1614
1913
|
}
|
|
1914
|
+
catch {
|
|
1915
|
+
// best-effort: settlement must not break session start
|
|
1916
|
+
}
|
|
1615
1917
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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);
|
|
1619
1925
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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 () => {
|
|
1629
1941
|
try {
|
|
1630
|
-
|
|
1631
|
-
syncSharedLayer();
|
|
1632
|
-
}
|
|
1942
|
+
await Promise.allSettled([...pending]);
|
|
1633
1943
|
}
|
|
1634
|
-
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
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
|
+
}
|
|
1637
1963
|
}
|
|
1638
|
-
}
|
|
1639
|
-
metrics.flush();
|
|
1964
|
+
});
|
|
1640
1965
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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);
|
|
1647
1973
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1974
|
+
catch {
|
|
1975
|
+
// best-effort: pre-011 DBs have no perf_samples table
|
|
1976
|
+
}
|
|
1977
|
+
metrics.close();
|
|
1978
|
+
store.close();
|
|
1653
1979
|
}
|
|
1654
1980
|
},
|
|
1655
|
-
|
|
1656
|
-
await Promise.allSettled([...pending]);
|
|
1657
|
-
metrics.close();
|
|
1658
|
-
store.close();
|
|
1659
|
-
},
|
|
1660
|
-
};
|
|
1981
|
+
});
|
|
1661
1982
|
};
|
|
1662
1983
|
export default KevinPlugin;
|
|
1663
|
-
//# sourceMappingURL=index.js.map
|