selftune 0.2.6 → 0.2.8
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 +1 -0
- package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +15 -0
- package/apps/local-dashboard/dist/assets/index-CRtLkBTi.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +60 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-B7VF2Ipl.js → vendor-table-dK1QMLq9.js} +1 -1
- package/apps/local-dashboard/dist/assets/{vendor-ui-r2k_Ku_V.js → vendor-ui-CO2mrx6e.js} +60 -65
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/activation-rules.ts +30 -9
- package/cli/selftune/agent-guidance.ts +96 -0
- package/cli/selftune/alpha-identity.ts +157 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +151 -0
- package/cli/selftune/alpha-upload/client.ts +113 -0
- package/cli/selftune/alpha-upload/flush.ts +191 -0
- package/cli/selftune/alpha-upload/index.ts +194 -0
- package/cli/selftune/alpha-upload/queue.ts +252 -0
- package/cli/selftune/alpha-upload/stage-canonical.ts +242 -0
- package/cli/selftune/alpha-upload-contract.ts +52 -0
- package/cli/selftune/auth/device-code.ts +110 -0
- package/cli/selftune/auto-update.ts +130 -0
- package/cli/selftune/badge/badge.ts +19 -9
- package/cli/selftune/canonical-export.ts +16 -3
- package/cli/selftune/constants.ts +28 -8
- package/cli/selftune/contribute/bundle.ts +32 -5
- package/cli/selftune/dashboard-contract.ts +32 -1
- package/cli/selftune/dashboard-server.ts +256 -692
- package/cli/selftune/dashboard.ts +1 -1
- package/cli/selftune/eval/baseline.ts +11 -7
- package/cli/selftune/eval/hooks-to-evals.ts +27 -9
- package/cli/selftune/eval/synthetic-evals.ts +54 -1
- package/cli/selftune/evolution/audit.ts +24 -19
- package/cli/selftune/evolution/constitutional.ts +176 -0
- package/cli/selftune/evolution/evidence.ts +18 -13
- package/cli/selftune/evolution/evolve-body.ts +104 -7
- package/cli/selftune/evolution/evolve.ts +195 -22
- package/cli/selftune/evolution/propose-body.ts +18 -1
- package/cli/selftune/evolution/propose-description.ts +27 -2
- package/cli/selftune/evolution/rollback.ts +11 -15
- package/cli/selftune/export.ts +84 -0
- package/cli/selftune/grading/auto-grade.ts +13 -4
- package/cli/selftune/grading/grade-session.ts +16 -6
- package/cli/selftune/hooks/evolution-guard.ts +26 -9
- package/cli/selftune/hooks/prompt-log.ts +23 -9
- package/cli/selftune/hooks/session-stop.ts +78 -15
- package/cli/selftune/hooks/skill-eval.ts +189 -10
- package/cli/selftune/index.ts +274 -2
- package/cli/selftune/ingestors/claude-replay.ts +48 -21
- package/cli/selftune/init.ts +249 -47
- package/cli/selftune/last.ts +7 -7
- package/cli/selftune/localdb/db.ts +90 -10
- package/cli/selftune/localdb/direct-write.ts +531 -0
- package/cli/selftune/localdb/materialize.ts +296 -42
- package/cli/selftune/localdb/queries.ts +325 -32
- package/cli/selftune/localdb/schema.ts +109 -0
- package/cli/selftune/monitoring/watch.ts +26 -8
- package/cli/selftune/normalization.ts +85 -15
- package/cli/selftune/observability.ts +248 -2
- package/cli/selftune/orchestrate.ts +165 -20
- package/cli/selftune/quickstart.ts +34 -10
- package/cli/selftune/repair/skill-usage.ts +12 -2
- package/cli/selftune/routes/actions.ts +77 -0
- package/cli/selftune/routes/badge.ts +66 -0
- package/cli/selftune/routes/doctor.ts +12 -0
- package/cli/selftune/routes/index.ts +14 -0
- package/cli/selftune/routes/orchestrate-runs.ts +13 -0
- package/cli/selftune/routes/overview.ts +14 -0
- package/cli/selftune/routes/report.ts +293 -0
- package/cli/selftune/routes/skill-report.ts +230 -0
- package/cli/selftune/status.ts +203 -7
- package/cli/selftune/sync.ts +13 -1
- package/cli/selftune/types.ts +50 -0
- package/cli/selftune/utils/jsonl.ts +58 -1
- package/cli/selftune/utils/selftune-meta.ts +38 -0
- package/cli/selftune/utils/skill-log.ts +30 -4
- package/cli/selftune/utils/transcript.ts +15 -0
- package/cli/selftune/workflows/workflows.ts +7 -6
- package/package.json +10 -6
- package/packages/telemetry-contract/fixtures/complete-push.ts +184 -0
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +58 -0
- package/packages/telemetry-contract/fixtures/golden.json +1 -0
- package/packages/telemetry-contract/fixtures/index.ts +4 -0
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +40 -0
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +79 -0
- package/packages/telemetry-contract/package.json +6 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +215 -0
- package/packages/telemetry-contract/src/types.ts +3 -1
- package/packages/telemetry-contract/src/validators.ts +3 -1
- package/packages/telemetry-contract/tests/compatibility.test.ts +144 -0
- package/packages/ui/package.json +4 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +61 -29
- package/packages/ui/src/components/section-cards.tsx +31 -14
- package/packages/ui/src/types.ts +1 -0
- package/skill/SKILL.md +214 -174
- package/skill/Workflows/AlphaUpload.md +45 -0
- package/skill/Workflows/Baseline.md +18 -12
- package/skill/Workflows/Composability.md +3 -3
- package/skill/Workflows/Dashboard.md +44 -91
- package/skill/Workflows/Doctor.md +93 -66
- package/skill/Workflows/Evals.md +49 -40
- package/skill/Workflows/Evolve.md +76 -28
- package/skill/Workflows/EvolveBody.md +37 -38
- package/skill/Workflows/Initialize.md +172 -26
- package/skill/Workflows/Orchestrate.md +11 -2
- package/skill/Workflows/Sync.md +23 -0
- package/skill/Workflows/Watch.md +2 -5
- package/skill/agents/diagnosis-analyst.md +163 -0
- package/skill/agents/evolution-reviewer.md +149 -0
- package/skill/agents/integration-guide.md +154 -0
- package/skill/agents/pattern-analyst.md +149 -0
- package/skill/assets/multi-skill-settings.json +1 -1
- package/skill/assets/single-skill-settings.json +1 -1
- package/skill/references/interactive-config.md +39 -0
- package/skill/references/invocation-taxonomy.md +34 -0
- package/skill/references/logs.md +9 -1
- package/skill/references/setup-patterns.md +3 -3
- package/skill/settings_snippet.json +1 -1
- package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +0 -1
- package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +0 -15
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +0 -60
|
@@ -7,13 +7,20 @@
|
|
|
7
7
|
* - Incremental: only inserts records newer than last materialization
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// NOTE: With dual-write active (Phase 1+), hooks insert directly into SQLite.
|
|
11
|
+
// The materializer is only needed for:
|
|
12
|
+
// 1. Initial startup (to catch pre-existing JSONL data from before dual-write)
|
|
13
|
+
// 2. Manual recovery after exporting JSONL and recreating the DB file
|
|
14
|
+
// 3. Backfill from batch ingestors that don't yet dual-write
|
|
15
|
+
|
|
10
16
|
import type { Database } from "bun:sqlite";
|
|
11
|
-
import
|
|
12
|
-
CanonicalExecutionFactRecord,
|
|
13
|
-
CanonicalPromptRecord,
|
|
14
|
-
CanonicalRecord,
|
|
15
|
-
CanonicalSessionRecord,
|
|
16
|
-
CanonicalSkillInvocationRecord,
|
|
17
|
+
import {
|
|
18
|
+
type CanonicalExecutionFactRecord,
|
|
19
|
+
type CanonicalPromptRecord,
|
|
20
|
+
type CanonicalRecord,
|
|
21
|
+
type CanonicalSessionRecord,
|
|
22
|
+
type CanonicalSkillInvocationRecord,
|
|
23
|
+
isCanonicalRecord,
|
|
17
24
|
} from "@selftune/telemetry-contract";
|
|
18
25
|
import {
|
|
19
26
|
CANONICAL_LOG,
|
|
@@ -23,7 +30,6 @@ import {
|
|
|
23
30
|
TELEMETRY_LOG,
|
|
24
31
|
} from "../constants.js";
|
|
25
32
|
import type { OrchestrateRunReport } from "../dashboard-contract.js";
|
|
26
|
-
import { readEvidenceTrail } from "../evolution/evidence.js";
|
|
27
33
|
import type {
|
|
28
34
|
EvolutionAuditEntry,
|
|
29
35
|
EvolutionEvidenceEntry,
|
|
@@ -31,19 +37,134 @@ import type {
|
|
|
31
37
|
SkillUsageRecord,
|
|
32
38
|
} from "../types.js";
|
|
33
39
|
import { readCanonicalRecords } from "../utils/canonical-log.js";
|
|
34
|
-
import { readJsonl } from "../utils/jsonl.js";
|
|
40
|
+
import { readJsonl, readJsonlFrom } from "../utils/jsonl.js";
|
|
35
41
|
import { readEffectiveSkillUsageRecords } from "../utils/skill-log.js";
|
|
36
42
|
import { getMeta, setMeta } from "./db.js";
|
|
37
43
|
|
|
44
|
+
/** Tables that contain SQLite-only data (written by hooks, not just materialized from JSONL). */
|
|
45
|
+
const _PROTECTED_TABLES = [
|
|
46
|
+
{ table: "evolution_audit", tsColumn: "timestamp", jsonlLog: EVOLUTION_AUDIT_LOG },
|
|
47
|
+
{ table: "evolution_evidence", tsColumn: "timestamp", jsonlLog: EVOLUTION_EVIDENCE_LOG },
|
|
48
|
+
{ table: "orchestrate_runs", tsColumn: "timestamp", jsonlLog: ORCHESTRATE_RUN_LOG },
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Preflight check before full rebuild: detect tables where SQLite has rows
|
|
53
|
+
* newer than the corresponding JSONL file. If found and `force` is not set,
|
|
54
|
+
* throw an error so the user can export first.
|
|
55
|
+
*/
|
|
56
|
+
function preflightRebuildGuard(db: Database, options?: MaterializeOptions): void {
|
|
57
|
+
if (options?.force) return;
|
|
58
|
+
|
|
59
|
+
const protectedTables = [
|
|
60
|
+
{
|
|
61
|
+
table: "evolution_audit",
|
|
62
|
+
tsColumn: "timestamp",
|
|
63
|
+
jsonlLog: options?.evolutionAuditPath ?? EVOLUTION_AUDIT_LOG,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
table: "evolution_evidence",
|
|
67
|
+
tsColumn: "timestamp",
|
|
68
|
+
jsonlLog: options?.evolutionEvidencePath ?? EVOLUTION_EVIDENCE_LOG,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
table: "orchestrate_runs",
|
|
72
|
+
tsColumn: "timestamp",
|
|
73
|
+
jsonlLog: options?.orchestrateRunLogPath ?? ORCHESTRATE_RUN_LOG,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const warnings: string[] = [];
|
|
78
|
+
for (const { table, tsColumn, jsonlLog } of protectedTables) {
|
|
79
|
+
// Get newest timestamp in SQLite
|
|
80
|
+
let sqliteMax: string | null = null;
|
|
81
|
+
try {
|
|
82
|
+
const row = db.query(`SELECT MAX(${tsColumn}) AS max_ts FROM ${table}`).get() as {
|
|
83
|
+
max_ts: string | null;
|
|
84
|
+
} | null;
|
|
85
|
+
sqliteMax = row?.max_ts ?? null;
|
|
86
|
+
} catch {
|
|
87
|
+
continue; // table doesn't exist yet — safe to rebuild
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!sqliteMax) continue; // no rows in SQLite — safe
|
|
91
|
+
|
|
92
|
+
// Get newest timestamp from JSONL
|
|
93
|
+
let jsonlMax: string | null = null;
|
|
94
|
+
let jsonlBoundaryCount = 0;
|
|
95
|
+
try {
|
|
96
|
+
const records = readJsonl<{ timestamp: string }>(jsonlLog);
|
|
97
|
+
if (records.length > 0) {
|
|
98
|
+
jsonlMax = records.reduce(
|
|
99
|
+
(max, r) => (r.timestamp > max ? r.timestamp : max),
|
|
100
|
+
records[0].timestamp,
|
|
101
|
+
);
|
|
102
|
+
jsonlBoundaryCount = records.filter((record) => record.timestamp === jsonlMax).length;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// JSONL file doesn't exist or is empty — SQLite has data JSONL doesn't
|
|
106
|
+
jsonlMax = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let newerCount = 0;
|
|
110
|
+
let sqliteBoundaryCount = 0;
|
|
111
|
+
try {
|
|
112
|
+
if (!jsonlMax) {
|
|
113
|
+
const row = db.query(`SELECT COUNT(*) AS newer_count FROM ${table}`).get() as {
|
|
114
|
+
newer_count: number;
|
|
115
|
+
} | null;
|
|
116
|
+
newerCount = row?.newer_count ?? 0;
|
|
117
|
+
} else if (sqliteMax > jsonlMax) {
|
|
118
|
+
const row = db
|
|
119
|
+
.query(`SELECT COUNT(*) AS newer_count FROM ${table} WHERE ${tsColumn} > ?`)
|
|
120
|
+
.get(jsonlMax) as {
|
|
121
|
+
newer_count: number;
|
|
122
|
+
} | null;
|
|
123
|
+
newerCount = row?.newer_count ?? 0;
|
|
124
|
+
}
|
|
125
|
+
if (jsonlMax) {
|
|
126
|
+
const boundaryRow = db
|
|
127
|
+
.query(`SELECT COUNT(*) AS boundary_count FROM ${table} WHERE ${tsColumn} = ?`)
|
|
128
|
+
.get(jsonlMax) as {
|
|
129
|
+
boundary_count: number;
|
|
130
|
+
} | null;
|
|
131
|
+
sqliteBoundaryCount = boundaryRow?.boundary_count ?? 0;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
newerCount = 0;
|
|
135
|
+
sqliteBoundaryCount = 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
!jsonlMax ||
|
|
140
|
+
newerCount > 0 ||
|
|
141
|
+
(sqliteMax === jsonlMax && sqliteBoundaryCount !== jsonlBoundaryCount)
|
|
142
|
+
) {
|
|
143
|
+
warnings.push(
|
|
144
|
+
` - ${table}: ${newerCount} SQLite-only row(s), SQLite max=${sqliteMax}, JSONL max=${jsonlMax ?? "(empty)"}, boundary_count(SQLite=${sqliteBoundaryCount}, JSONL=${jsonlBoundaryCount})`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (warnings.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Rebuild blocked: the following tables have SQLite-only rows that would be lost:\n${warnings.join("\n")}\n\nRun \`selftune export\` first to preserve this data, then retry with --force.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
38
156
|
/** Meta key tracking last materialization timestamp. */
|
|
39
157
|
const META_LAST_MATERIALIZED = "last_materialized_at";
|
|
158
|
+
/** Meta key prefix for per-file byte offsets (append-only incremental reads). */
|
|
159
|
+
const META_OFFSET_PREFIX = "file_offset:";
|
|
40
160
|
|
|
41
161
|
/**
|
|
42
162
|
* Full rebuild: drop all data tables, then re-insert everything.
|
|
43
163
|
*/
|
|
44
164
|
export function materializeFull(db: Database, options?: MaterializeOptions): MaterializeResult {
|
|
165
|
+
preflightRebuildGuard(db, options);
|
|
166
|
+
|
|
45
167
|
const tables = [
|
|
46
|
-
"skill_usage",
|
|
47
168
|
"session_telemetry",
|
|
48
169
|
"evolution_audit",
|
|
49
170
|
"evolution_evidence",
|
|
@@ -56,6 +177,8 @@ export function materializeFull(db: Database, options?: MaterializeOptions): Mat
|
|
|
56
177
|
for (const table of tables) {
|
|
57
178
|
db.run(`DELETE FROM ${table}`);
|
|
58
179
|
}
|
|
180
|
+
// Clear byte offsets so full rebuild reads from start of each file
|
|
181
|
+
db.run("DELETE FROM _meta WHERE key LIKE ?", [`${META_OFFSET_PREFIX}%`]);
|
|
59
182
|
|
|
60
183
|
return materializeIncremental(db, { ...options, since: null });
|
|
61
184
|
}
|
|
@@ -67,6 +190,8 @@ export interface MaterializeOptions {
|
|
|
67
190
|
evolutionEvidencePath?: string;
|
|
68
191
|
orchestrateRunLogPath?: string;
|
|
69
192
|
since?: string | null;
|
|
193
|
+
/** Skip the preflight rebuild guard (use after `selftune export`). */
|
|
194
|
+
force?: boolean;
|
|
70
195
|
}
|
|
71
196
|
|
|
72
197
|
export interface MaterializeResult {
|
|
@@ -105,11 +230,30 @@ export function materializeIncremental(
|
|
|
105
230
|
orchestrateRuns: 0,
|
|
106
231
|
};
|
|
107
232
|
|
|
108
|
-
// -- Read
|
|
109
|
-
//
|
|
233
|
+
// -- Read only NEW data using byte offsets -----------------------------------
|
|
234
|
+
// Append-only JSONL files: track byte offset per file in _meta so we only
|
|
235
|
+
// read bytes appended since the last materialization. Falls back to full
|
|
236
|
+
// read when since is null (first run / full rebuild).
|
|
110
237
|
|
|
111
|
-
|
|
112
|
-
|
|
238
|
+
function getOffset(filePath: string): number {
|
|
239
|
+
if (!since) return 0; // full rebuild — read everything
|
|
240
|
+
const raw = getMeta(db, `${META_OFFSET_PREFIX}${filePath}`);
|
|
241
|
+
return raw ? Number.parseInt(raw, 10) : 0;
|
|
242
|
+
}
|
|
243
|
+
const newOffsets: Array<[string, number]> = [];
|
|
244
|
+
|
|
245
|
+
const canonicalPath = options?.canonicalLogPath ?? CANONICAL_LOG;
|
|
246
|
+
let filteredCanonical: CanonicalRecord[];
|
|
247
|
+
if (!since) {
|
|
248
|
+
filteredCanonical = readCanonicalRecords(canonicalPath);
|
|
249
|
+
} else {
|
|
250
|
+
const { records, newOffset } = readJsonlFrom<CanonicalRecord>(
|
|
251
|
+
canonicalPath,
|
|
252
|
+
getOffset(canonicalPath),
|
|
253
|
+
);
|
|
254
|
+
filteredCanonical = records.filter(isCanonicalRecord);
|
|
255
|
+
newOffsets.push([canonicalPath, newOffset]);
|
|
256
|
+
}
|
|
113
257
|
|
|
114
258
|
// Pre-partition canonical records by kind (single pass instead of 4x full scan)
|
|
115
259
|
const byKind = new Map<string, CanonicalRecord[]>();
|
|
@@ -119,27 +263,63 @@ export function materializeIncremental(
|
|
|
119
263
|
else byKind.set(r.record_kind, [r]);
|
|
120
264
|
}
|
|
121
265
|
|
|
122
|
-
const
|
|
123
|
-
|
|
266
|
+
const telemetryPath = options?.telemetryLogPath ?? TELEMETRY_LOG;
|
|
267
|
+
let filteredTelemetry: SessionTelemetryRecord[];
|
|
268
|
+
if (!since) {
|
|
269
|
+
filteredTelemetry = readJsonl<SessionTelemetryRecord>(telemetryPath);
|
|
270
|
+
} else {
|
|
271
|
+
const { records, newOffset } = readJsonlFrom<SessionTelemetryRecord>(
|
|
272
|
+
telemetryPath,
|
|
273
|
+
getOffset(telemetryPath),
|
|
274
|
+
);
|
|
275
|
+
filteredTelemetry = records;
|
|
276
|
+
newOffsets.push([telemetryPath, newOffset]);
|
|
277
|
+
}
|
|
124
278
|
|
|
279
|
+
// Skill usage uses a merge of raw + repaired logs — always full read
|
|
280
|
+
// since readEffectiveSkillUsageRecords handles dedup internally.
|
|
281
|
+
// However, when doing incremental, filter by timestamp.
|
|
125
282
|
const skills = readEffectiveSkillUsageRecords();
|
|
126
283
|
const filteredSkills = since ? skills.filter((r) => r.timestamp > since) : skills;
|
|
127
284
|
|
|
128
|
-
const
|
|
129
|
-
|
|
285
|
+
const auditPath = options?.evolutionAuditPath ?? EVOLUTION_AUDIT_LOG;
|
|
286
|
+
let filteredAudit: EvolutionAuditEntry[];
|
|
287
|
+
if (!since) {
|
|
288
|
+
filteredAudit = readJsonl<EvolutionAuditEntry>(auditPath);
|
|
289
|
+
} else {
|
|
290
|
+
const { records, newOffset } = readJsonlFrom<EvolutionAuditEntry>(
|
|
291
|
+
auditPath,
|
|
292
|
+
getOffset(auditPath),
|
|
293
|
+
);
|
|
294
|
+
filteredAudit = records;
|
|
295
|
+
newOffsets.push([auditPath, newOffset]);
|
|
296
|
+
}
|
|
130
297
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
298
|
+
const evidencePath = options?.evolutionEvidencePath ?? EVOLUTION_EVIDENCE_LOG;
|
|
299
|
+
let filteredEvidence: EvolutionEvidenceEntry[];
|
|
300
|
+
if (!since) {
|
|
301
|
+
filteredEvidence = readJsonl<EvolutionEvidenceEntry>(evidencePath);
|
|
302
|
+
} else {
|
|
303
|
+
const { records, newOffset } = readJsonlFrom<EvolutionEvidenceEntry>(
|
|
304
|
+
evidencePath,
|
|
305
|
+
getOffset(evidencePath),
|
|
306
|
+
);
|
|
307
|
+
filteredEvidence = records;
|
|
308
|
+
newOffsets.push([evidencePath, newOffset]);
|
|
309
|
+
}
|
|
136
310
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
311
|
+
const orchestratePath = options?.orchestrateRunLogPath ?? ORCHESTRATE_RUN_LOG;
|
|
312
|
+
let filteredOrchestrateRuns: OrchestrateRunReport[];
|
|
313
|
+
if (!since) {
|
|
314
|
+
filteredOrchestrateRuns = readJsonl<OrchestrateRunReport>(orchestratePath);
|
|
315
|
+
} else {
|
|
316
|
+
const { records, newOffset } = readJsonlFrom<OrchestrateRunReport>(
|
|
317
|
+
orchestratePath,
|
|
318
|
+
getOffset(orchestratePath),
|
|
319
|
+
);
|
|
320
|
+
filteredOrchestrateRuns = records;
|
|
321
|
+
newOffsets.push([orchestratePath, newOffset]);
|
|
322
|
+
}
|
|
143
323
|
|
|
144
324
|
// -- Insert everything inside a single transaction --------------------------
|
|
145
325
|
db.run("BEGIN TRANSACTION");
|
|
@@ -154,6 +334,10 @@ export function materializeIncremental(
|
|
|
154
334
|
result.evolutionEvidence = insertEvolutionEvidence(db, filteredEvidence);
|
|
155
335
|
result.orchestrateRuns = insertOrchestrateRuns(db, filteredOrchestrateRuns);
|
|
156
336
|
|
|
337
|
+
// Persist byte offsets so next incremental run skips already-read data
|
|
338
|
+
for (const [filePath, offset] of newOffsets) {
|
|
339
|
+
setMeta(db, `${META_OFFSET_PREFIX}${filePath}`, String(offset));
|
|
340
|
+
}
|
|
157
341
|
setMeta(db, META_LAST_MATERIALIZED, now);
|
|
158
342
|
db.run("COMMIT");
|
|
159
343
|
} catch (err) {
|
|
@@ -167,12 +351,24 @@ export function materializeIncremental(
|
|
|
167
351
|
// -- Insert helpers -----------------------------------------------------------
|
|
168
352
|
|
|
169
353
|
function insertSessions(db: Database, records: CanonicalRecord[]): number {
|
|
354
|
+
// Use upsert to merge non-null fields from duplicate session records.
|
|
355
|
+
// Multiple canonical records may exist for the same session (e.g., Stop hook
|
|
356
|
+
// writes one without model, replay ingestor writes another with model).
|
|
170
357
|
const stmt = db.prepare(`
|
|
171
|
-
INSERT
|
|
358
|
+
INSERT INTO sessions
|
|
172
359
|
(session_id, started_at, ended_at, platform, model, completion_status,
|
|
173
360
|
source_session_kind, agent_cli, workspace_path, repo_remote, branch,
|
|
174
361
|
schema_version, normalized_at)
|
|
175
362
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
363
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
364
|
+
started_at = COALESCE(sessions.started_at, excluded.started_at),
|
|
365
|
+
ended_at = COALESCE(sessions.ended_at, excluded.ended_at),
|
|
366
|
+
model = COALESCE(sessions.model, excluded.model),
|
|
367
|
+
completion_status = COALESCE(sessions.completion_status, excluded.completion_status),
|
|
368
|
+
agent_cli = COALESCE(sessions.agent_cli, excluded.agent_cli),
|
|
369
|
+
repo_remote = COALESCE(sessions.repo_remote, excluded.repo_remote),
|
|
370
|
+
branch = COALESCE(sessions.branch, excluded.branch),
|
|
371
|
+
workspace_path = COALESCE(sessions.workspace_path, excluded.workspace_path)
|
|
176
372
|
`);
|
|
177
373
|
|
|
178
374
|
let count = 0;
|
|
@@ -223,16 +419,31 @@ function insertPrompts(db: Database, records: CanonicalRecord[]): number {
|
|
|
223
419
|
}
|
|
224
420
|
|
|
225
421
|
function insertSkillInvocations(db: Database, records: CanonicalRecord[]): number {
|
|
422
|
+
// Ensure session stubs exist for FK satisfaction — hooks may write
|
|
423
|
+
// skill_invocation records before a full session record is available.
|
|
424
|
+
const sessionStub = db.prepare(`
|
|
425
|
+
INSERT OR IGNORE INTO sessions
|
|
426
|
+
(session_id, platform, schema_version, normalized_at)
|
|
427
|
+
VALUES (?, ?, ?, ?)
|
|
428
|
+
`);
|
|
429
|
+
|
|
226
430
|
const stmt = db.prepare(`
|
|
227
431
|
INSERT OR IGNORE INTO skill_invocations
|
|
228
432
|
(skill_invocation_id, session_id, occurred_at, skill_name, invocation_mode,
|
|
229
|
-
triggered, confidence, tool_name, matched_prompt_id
|
|
230
|
-
|
|
433
|
+
triggered, confidence, tool_name, matched_prompt_id, agent_type,
|
|
434
|
+
query, skill_path, skill_scope, source)
|
|
435
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
231
436
|
`);
|
|
232
437
|
|
|
233
438
|
let count = 0;
|
|
234
439
|
for (const r of records) {
|
|
235
440
|
const si = r as CanonicalSkillInvocationRecord;
|
|
441
|
+
sessionStub.run(
|
|
442
|
+
si.session_id,
|
|
443
|
+
si.platform ?? "unknown",
|
|
444
|
+
si.schema_version ?? "1.0.0",
|
|
445
|
+
si.normalized_at ?? new Date().toISOString(),
|
|
446
|
+
);
|
|
236
447
|
stmt.run(
|
|
237
448
|
si.skill_invocation_id,
|
|
238
449
|
si.session_id,
|
|
@@ -243,6 +454,11 @@ function insertSkillInvocations(db: Database, records: CanonicalRecord[]): numbe
|
|
|
243
454
|
si.confidence,
|
|
244
455
|
si.tool_name ?? null,
|
|
245
456
|
si.matched_prompt_id ?? null,
|
|
457
|
+
si.agent_type ?? null,
|
|
458
|
+
((si as Record<string, unknown>).query as string) ?? null,
|
|
459
|
+
((si as Record<string, unknown>).skill_path as string) ?? null,
|
|
460
|
+
((si as Record<string, unknown>).skill_scope as string) ?? null,
|
|
461
|
+
((si as Record<string, unknown>).source as string) ?? null,
|
|
246
462
|
);
|
|
247
463
|
count++;
|
|
248
464
|
}
|
|
@@ -281,12 +497,29 @@ function insertExecutionFacts(db: Database, records: CanonicalRecord[]): number
|
|
|
281
497
|
|
|
282
498
|
function insertSessionTelemetry(db: Database, records: SessionTelemetryRecord[]): number {
|
|
283
499
|
const stmt = db.prepare(`
|
|
284
|
-
INSERT
|
|
500
|
+
INSERT INTO session_telemetry
|
|
285
501
|
(session_id, timestamp, cwd, transcript_path, tool_calls_json,
|
|
286
502
|
total_tool_calls, bash_commands_json, skills_triggered_json,
|
|
287
503
|
skills_invoked_json, assistant_turns, errors_encountered,
|
|
288
504
|
transcript_chars, last_user_query, source, input_tokens, output_tokens)
|
|
289
505
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
506
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
507
|
+
timestamp = excluded.timestamp,
|
|
508
|
+
cwd = COALESCE(excluded.cwd, session_telemetry.cwd),
|
|
509
|
+
transcript_path = COALESCE(excluded.transcript_path, session_telemetry.transcript_path),
|
|
510
|
+
source = COALESCE(excluded.source, session_telemetry.source),
|
|
511
|
+
tool_calls_json = excluded.tool_calls_json,
|
|
512
|
+
total_tool_calls = excluded.total_tool_calls,
|
|
513
|
+
bash_commands_json = excluded.bash_commands_json,
|
|
514
|
+
skills_triggered_json = COALESCE(excluded.skills_triggered_json, session_telemetry.skills_triggered_json),
|
|
515
|
+
skills_invoked_json = COALESCE(excluded.skills_invoked_json, session_telemetry.skills_invoked_json),
|
|
516
|
+
assistant_turns = excluded.assistant_turns,
|
|
517
|
+
errors_encountered = excluded.errors_encountered,
|
|
518
|
+
transcript_chars = excluded.transcript_chars,
|
|
519
|
+
last_user_query = excluded.last_user_query,
|
|
520
|
+
input_tokens = COALESCE(excluded.input_tokens, session_telemetry.input_tokens),
|
|
521
|
+
output_tokens = COALESCE(excluded.output_tokens, session_telemetry.output_tokens)
|
|
522
|
+
WHERE session_telemetry.timestamp IS NULL OR excluded.timestamp >= session_telemetry.timestamp
|
|
290
523
|
`);
|
|
291
524
|
|
|
292
525
|
let count = 0;
|
|
@@ -315,24 +548,44 @@ function insertSessionTelemetry(db: Database, records: SessionTelemetryRecord[])
|
|
|
315
548
|
}
|
|
316
549
|
|
|
317
550
|
function insertSkillUsage(db: Database, records: SkillUsageRecord[]): number {
|
|
318
|
-
//
|
|
319
|
-
//
|
|
551
|
+
// Skill usage records now go into the unified skill_invocations table.
|
|
552
|
+
// Uses INSERT OR IGNORE with the dedup index on skill_invocations.
|
|
553
|
+
const sessionStub = db.prepare(`
|
|
554
|
+
INSERT OR IGNORE INTO sessions
|
|
555
|
+
(session_id, platform, schema_version, normalized_at)
|
|
556
|
+
VALUES (?, ?, ?, ?)
|
|
557
|
+
`);
|
|
558
|
+
|
|
320
559
|
const stmt = db.prepare(`
|
|
321
|
-
INSERT OR IGNORE INTO
|
|
322
|
-
(
|
|
323
|
-
|
|
560
|
+
INSERT OR IGNORE INTO skill_invocations
|
|
561
|
+
(skill_invocation_id, session_id, occurred_at, skill_name, invocation_mode,
|
|
562
|
+
triggered, confidence, tool_name, matched_prompt_id, agent_type,
|
|
563
|
+
query, skill_path, skill_scope, source)
|
|
564
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
324
565
|
`);
|
|
325
566
|
|
|
326
567
|
let count = 0;
|
|
327
568
|
for (const r of records) {
|
|
569
|
+
// Ensure session stub exists for FK satisfaction
|
|
570
|
+
sessionStub.run(r.session_id, "unknown", "1.0.0", new Date().toISOString());
|
|
571
|
+
|
|
572
|
+
// Derive a unique skill_invocation_id for skill_usage records
|
|
573
|
+
const invocationId = `${r.session_id}:su:${r.timestamp}:${r.skill_name}`;
|
|
574
|
+
|
|
328
575
|
stmt.run(
|
|
329
|
-
|
|
576
|
+
invocationId,
|
|
330
577
|
r.session_id,
|
|
578
|
+
r.timestamp, // timestamp → occurred_at
|
|
331
579
|
r.skill_name,
|
|
580
|
+
null, // invocation_mode — not available from skill_usage
|
|
581
|
+
r.triggered ? 1 : 0,
|
|
582
|
+
null, // confidence — not available from skill_usage
|
|
583
|
+
null, // tool_name — not available from skill_usage
|
|
584
|
+
null, // matched_prompt_id — not available from skill_usage
|
|
585
|
+
null, // agent_type — not available from skill_usage
|
|
586
|
+
r.query,
|
|
332
587
|
r.skill_path,
|
|
333
588
|
r.skill_scope ?? null,
|
|
334
|
-
r.query,
|
|
335
|
-
r.triggered ? 1 : 0,
|
|
336
589
|
r.source ?? null,
|
|
337
590
|
);
|
|
338
591
|
count++;
|
|
@@ -345,8 +598,8 @@ function insertEvolutionAudit(db: Database, records: EvolutionAuditEntry[]): num
|
|
|
345
598
|
// (idx_evo_audit_dedup defined in schema.ts).
|
|
346
599
|
const stmt = db.prepare(`
|
|
347
600
|
INSERT OR IGNORE INTO evolution_audit
|
|
348
|
-
(timestamp, proposal_id, skill_name, action, details, eval_snapshot_json)
|
|
349
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
601
|
+
(timestamp, proposal_id, skill_name, action, details, eval_snapshot_json, iterations_used)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
350
603
|
`);
|
|
351
604
|
|
|
352
605
|
let count = 0;
|
|
@@ -358,6 +611,7 @@ function insertEvolutionAudit(db: Database, records: EvolutionAuditEntry[]): num
|
|
|
358
611
|
r.action,
|
|
359
612
|
r.details,
|
|
360
613
|
r.eval_snapshot ? JSON.stringify(r.eval_snapshot) : null,
|
|
614
|
+
r.iterations_used ?? null,
|
|
361
615
|
);
|
|
362
616
|
count++;
|
|
363
617
|
}
|