loki-mode 10.5.35 → 10.6.6
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 +15 -308
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +34 -1
- package/autonomy/telemetry.sh +1 -1
- package/bin/loki +5 -5
- package/completions/_loki +1 -0
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +6 -5
- package/docs/v10/BOARD.md +1 -0
- package/docs/v10/CONTROL-PLANE.md +102 -0
- package/docs/v10/DECISIONS.md +33 -0
- package/docs/v10/DEPS.md +16 -0
- package/docs/v10/LEGACY-REMOVAL.md +101 -0
- package/docs/v10/OPERATING-MODEL.md +1 -1
- package/docs/v10/PROGRESS.md +6 -0
- package/loki-ts/dist/loki.js +485 -464
- package/mcp/__init__.py +1 -1
- package/package.json +14 -3
- package/packages/control-plane/dist/server.js +5919 -0
- package/packages/control-plane/drizzle/0000_new_maelstrom.sql +49 -0
- package/packages/control-plane/drizzle/meta/0000_snapshot.json +343 -0
- package/packages/control-plane/drizzle/meta/_journal.json +13 -0
- package/packages/control-plane/package.json +20 -0
- package/packages/control-plane/src/db/migrate.ts +20 -0
- package/packages/control-plane/src/db/schema.ts +54 -0
- package/packages/control-plane/src/server/app.ts +63 -0
- package/packages/control-plane/src/server/ingest.ts +52 -0
- package/packages/control-plane/src/server/runs.ts +116 -0
- package/packages/control-plane/src/server/serve.ts +15 -0
- package/packages/control-plane/src/shipper/backfill.ts +40 -0
- package/packages/control-plane/src/shipper/ship.ts +95 -0
- package/packages/control-plane/ui/dist/assets/index-B6EiyukW.js +114 -0
- package/packages/control-plane/ui/dist/assets/index-BH3LtKcG.css +1 -0
- package/packages/control-plane/ui/dist/index.html +13 -0
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-DEZMrOU2.js → AdminPage-CH-bmw4l.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dcn23Doz.js → Avatar-Bos-ky_x.js} +1 -1
- package/web-app/dist/assets/{Badge-CsV7PB4a.js → Badge-DNJyHg9Y.js} +1 -1
- package/web-app/dist/assets/{Button-C8Q1ckXg.js → Button-KqD3Tt-I.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-YYX4FOT9.js → CockpitPage-l6oTsRaL.js} +1 -1
- package/web-app/dist/assets/{ComparePage-BrA6JGOn.js → ComparePage-DN6uxXko.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CA3xFm23.js → ErrorBoundary-CKQhnvU9.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C9151UPM.js → EvidenceReceiptPanel-CzSIqLmy.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DBYUDlkc.js → GitHubIssuesPanel-YJUcDezn.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-x-Ug3n6Q.js → GitHubPRsPanel-Bv7xFvJN.js} +1 -1
- package/web-app/dist/assets/{HomePage-DHPokDA2.js → HomePage-CJsxBbii.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CB8h5M-E.js → LoginPage-D1V2Ag0s.js} +1 -1
- package/web-app/dist/assets/{MagicPage-B6Zg4DHE.js → MagicPage-BBVNMEr5.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-JN6AkYit.js → MetricsPage-CtHM-IlA.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-dOUCtSdt.js → NotFoundPage-BBEEJLj5.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DfyhdFcg.js → ProjectPage-BVRJoEaQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-oHMOXioV.js → ProjectsPage-DqaA9rUa.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-D4BGbBbt.js → SettingsPage-B_RQV_vF.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CCkk1kPK.js → ShowcasePage-DqN-mrZ8.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-N2_JdIbZ.js → SystemSettingsPage-D-CvuL-K.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-CEDSFaIj.js → TeamsPage-CzkkDcPj.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DB9WjPjI.js → TemplatesPage-Bbchk7YQ.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DK55nBdV.js → TerminalOutput-B32BAX--.js} +1 -1
- package/web-app/dist/assets/{activity-3EPj3lfE.js → activity-C9vnEmhG.js} +1 -1
- package/web-app/dist/assets/{bell-05FwE2g4.js → bell-XsIhkyjq.js} +1 -1
- package/web-app/dist/assets/{bot-DAtzuPmj.js → bot-CvQhqBws.js} +1 -1
- package/web-app/dist/assets/{check-DVZjl88M.js → check-HmG8WcD-.js} +1 -1
- package/web-app/dist/assets/{chevron-left-BaNjYaIh.js → chevron-left-C1KNgnbD.js} +1 -1
- package/web-app/dist/assets/{circle-alert-DlvI6QdR.js → circle-alert-DSmCvZju.js} +1 -1
- package/web-app/dist/assets/{clock-B0vWVEQz.js → clock-CPhE3RXd.js} +1 -1
- package/web-app/dist/assets/{cloud-Du5W0a4Q.js → cloud-29lwqocB.js} +1 -1
- package/web-app/dist/assets/{code-xml-CBNJDFTV.js → code-xml-4jZUzbPt.js} +1 -1
- package/web-app/dist/assets/{database-CCqFWW7j.js → database-H0Ct2r-8.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-BiD3YO1u.js → dollar-sign-DsXkO0yT.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DCelfbWq.js → file-code-corner-BM8w_nvX.js} +1 -1
- package/web-app/dist/assets/{file-plus-B18v-CXD.js → file-plus-BWkYTJ4D.js} +1 -1
- package/web-app/dist/assets/{globe-DdaeBtBG.js → globe-FOFDl_8Z.js} +1 -1
- package/web-app/dist/assets/{hammer-BaxkIYAS.js → hammer-orm5gvky.js} +1 -1
- package/web-app/dist/assets/{index-BnsxsCEv.js → index-B6juCuPX.js} +3 -3
- package/web-app/dist/assets/{layers-jmRqcTgA.js → layers-DUQPkPAl.js} +1 -1
- package/web-app/dist/assets/{loader-circle-B5eWOgN-.js → loader-circle-Deq6bDar.js} +1 -1
- package/web-app/dist/assets/{lock-DCOObKS4.js → lock-CSzWTPRC.js} +1 -1
- package/web-app/dist/assets/{package-1nzEtHlZ.js → package-CmACgZuF.js} +1 -1
- package/web-app/dist/assets/{plus-B5cRKjki.js → plus-BjPW5v3A.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D769PBG8.js → refresh-cw-DhI1sHaj.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CTHsioqQ.js → rotate-ccw-Byi2ouWB.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DcwZwpH-.js → scroll-text-gq7bJ81H.js} +1 -1
- package/web-app/dist/assets/{server-DvIdr3uc.js → server-YbY5s1-X.js} +1 -1
- package/web-app/dist/assets/{shield-alert-HsBQ7gvc.js → shield-alert-DhKjtwZt.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Dwy2c2yM.js → thumbs-up-D2X5JR-5.js} +1 -1
- package/web-app/dist/assets/{trash-2-DhZM5W4J.js → trash-2-BHAmMxTq.js} +1 -1
- package/web-app/dist/assets/{trending-down-Bw3nDTCd.js → trending-down-C41kQa-Q.js} +1 -1
- package/web-app/dist/assets/{trending-up-CbZDPnht.js → trending-up-M-OK_9DC.js} +1 -1
- package/web-app/dist/assets/{upload-w6RYv_bT.js → upload-CarpPiGw.js} +1 -1
- package/web-app/dist/assets/{usePolling-9hkUOMWP.js → usePolling-rJ7rM7R0.js} +1 -1
- package/web-app/dist/assets/{user-Cct2X0U_.js → user-cBhWpZen.js} +1 -1
- package/web-app/dist/index.html +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
CREATE TABLE `events` (
|
|
2
|
+
`source_id` text NOT NULL,
|
|
3
|
+
`run_id` text NOT NULL,
|
|
4
|
+
`seq` integer NOT NULL,
|
|
5
|
+
`ts` text NOT NULL,
|
|
6
|
+
`type` text NOT NULL,
|
|
7
|
+
`stage` text,
|
|
8
|
+
`data` text NOT NULL,
|
|
9
|
+
`line_sha256` text NOT NULL,
|
|
10
|
+
`received_at` text NOT NULL,
|
|
11
|
+
PRIMARY KEY(`source_id`, `run_id`, `seq`)
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE INDEX `events_run_seq` ON `events` (`run_id`,`seq`);--> statement-breakpoint
|
|
15
|
+
CREATE INDEX `events_type_ts` ON `events` (`type`,`ts`);--> statement-breakpoint
|
|
16
|
+
CREATE TABLE `runs` (
|
|
17
|
+
`source_id` text NOT NULL,
|
|
18
|
+
`run_id` text NOT NULL,
|
|
19
|
+
`origin_repo` text,
|
|
20
|
+
`issue_ref` text,
|
|
21
|
+
`task_source` text,
|
|
22
|
+
`provider` text,
|
|
23
|
+
`model` text,
|
|
24
|
+
`started_at` text,
|
|
25
|
+
`ended_at` text,
|
|
26
|
+
`verdict` text,
|
|
27
|
+
`pr_url` text,
|
|
28
|
+
`pr_draft` integer,
|
|
29
|
+
`cost_usd` real,
|
|
30
|
+
`partial_usd` real NOT NULL,
|
|
31
|
+
`measured_sessions` integer NOT NULL,
|
|
32
|
+
`total_sessions` integer NOT NULL,
|
|
33
|
+
`input_tokens` integer NOT NULL,
|
|
34
|
+
`output_tokens` integer NOT NULL,
|
|
35
|
+
`wall_s` real,
|
|
36
|
+
`last_seq` integer NOT NULL,
|
|
37
|
+
`last_event_at` text,
|
|
38
|
+
`tampered` integer NOT NULL,
|
|
39
|
+
`conflict` integer DEFAULT 0 NOT NULL,
|
|
40
|
+
PRIMARY KEY(`source_id`, `run_id`)
|
|
41
|
+
);
|
|
42
|
+
--> statement-breakpoint
|
|
43
|
+
CREATE INDEX `runs_started` ON `runs` (`started_at`);--> statement-breakpoint
|
|
44
|
+
CREATE INDEX `runs_verdict` ON `runs` (`verdict`);--> statement-breakpoint
|
|
45
|
+
CREATE TABLE `sources` (
|
|
46
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
47
|
+
`first_seen` text NOT NULL,
|
|
48
|
+
`last_seen` text NOT NULL
|
|
49
|
+
);
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "6",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "32fa548f-e5a1-47e9-9a54-1be1f970a2de",
|
|
5
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
6
|
+
"tables": {
|
|
7
|
+
"events": {
|
|
8
|
+
"name": "events",
|
|
9
|
+
"columns": {
|
|
10
|
+
"source_id": {
|
|
11
|
+
"name": "source_id",
|
|
12
|
+
"type": "text",
|
|
13
|
+
"primaryKey": false,
|
|
14
|
+
"notNull": true,
|
|
15
|
+
"autoincrement": false
|
|
16
|
+
},
|
|
17
|
+
"run_id": {
|
|
18
|
+
"name": "run_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true,
|
|
22
|
+
"autoincrement": false
|
|
23
|
+
},
|
|
24
|
+
"seq": {
|
|
25
|
+
"name": "seq",
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true,
|
|
29
|
+
"autoincrement": false
|
|
30
|
+
},
|
|
31
|
+
"ts": {
|
|
32
|
+
"name": "ts",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"primaryKey": false,
|
|
35
|
+
"notNull": true,
|
|
36
|
+
"autoincrement": false
|
|
37
|
+
},
|
|
38
|
+
"type": {
|
|
39
|
+
"name": "type",
|
|
40
|
+
"type": "text",
|
|
41
|
+
"primaryKey": false,
|
|
42
|
+
"notNull": true,
|
|
43
|
+
"autoincrement": false
|
|
44
|
+
},
|
|
45
|
+
"stage": {
|
|
46
|
+
"name": "stage",
|
|
47
|
+
"type": "text",
|
|
48
|
+
"primaryKey": false,
|
|
49
|
+
"notNull": false,
|
|
50
|
+
"autoincrement": false
|
|
51
|
+
},
|
|
52
|
+
"data": {
|
|
53
|
+
"name": "data",
|
|
54
|
+
"type": "text",
|
|
55
|
+
"primaryKey": false,
|
|
56
|
+
"notNull": true,
|
|
57
|
+
"autoincrement": false
|
|
58
|
+
},
|
|
59
|
+
"line_sha256": {
|
|
60
|
+
"name": "line_sha256",
|
|
61
|
+
"type": "text",
|
|
62
|
+
"primaryKey": false,
|
|
63
|
+
"notNull": true,
|
|
64
|
+
"autoincrement": false
|
|
65
|
+
},
|
|
66
|
+
"received_at": {
|
|
67
|
+
"name": "received_at",
|
|
68
|
+
"type": "text",
|
|
69
|
+
"primaryKey": false,
|
|
70
|
+
"notNull": true,
|
|
71
|
+
"autoincrement": false
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"indexes": {
|
|
75
|
+
"events_run_seq": {
|
|
76
|
+
"name": "events_run_seq",
|
|
77
|
+
"columns": [
|
|
78
|
+
"run_id",
|
|
79
|
+
"seq"
|
|
80
|
+
],
|
|
81
|
+
"isUnique": false
|
|
82
|
+
},
|
|
83
|
+
"events_type_ts": {
|
|
84
|
+
"name": "events_type_ts",
|
|
85
|
+
"columns": [
|
|
86
|
+
"type",
|
|
87
|
+
"ts"
|
|
88
|
+
],
|
|
89
|
+
"isUnique": false
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"foreignKeys": {},
|
|
93
|
+
"compositePrimaryKeys": {
|
|
94
|
+
"events_source_id_run_id_seq_pk": {
|
|
95
|
+
"columns": [
|
|
96
|
+
"source_id",
|
|
97
|
+
"run_id",
|
|
98
|
+
"seq"
|
|
99
|
+
],
|
|
100
|
+
"name": "events_source_id_run_id_seq_pk"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"uniqueConstraints": {},
|
|
104
|
+
"checkConstraints": {}
|
|
105
|
+
},
|
|
106
|
+
"runs": {
|
|
107
|
+
"name": "runs",
|
|
108
|
+
"columns": {
|
|
109
|
+
"source_id": {
|
|
110
|
+
"name": "source_id",
|
|
111
|
+
"type": "text",
|
|
112
|
+
"primaryKey": false,
|
|
113
|
+
"notNull": true,
|
|
114
|
+
"autoincrement": false
|
|
115
|
+
},
|
|
116
|
+
"run_id": {
|
|
117
|
+
"name": "run_id",
|
|
118
|
+
"type": "text",
|
|
119
|
+
"primaryKey": false,
|
|
120
|
+
"notNull": true,
|
|
121
|
+
"autoincrement": false
|
|
122
|
+
},
|
|
123
|
+
"origin_repo": {
|
|
124
|
+
"name": "origin_repo",
|
|
125
|
+
"type": "text",
|
|
126
|
+
"primaryKey": false,
|
|
127
|
+
"notNull": false,
|
|
128
|
+
"autoincrement": false
|
|
129
|
+
},
|
|
130
|
+
"issue_ref": {
|
|
131
|
+
"name": "issue_ref",
|
|
132
|
+
"type": "text",
|
|
133
|
+
"primaryKey": false,
|
|
134
|
+
"notNull": false,
|
|
135
|
+
"autoincrement": false
|
|
136
|
+
},
|
|
137
|
+
"task_source": {
|
|
138
|
+
"name": "task_source",
|
|
139
|
+
"type": "text",
|
|
140
|
+
"primaryKey": false,
|
|
141
|
+
"notNull": false,
|
|
142
|
+
"autoincrement": false
|
|
143
|
+
},
|
|
144
|
+
"provider": {
|
|
145
|
+
"name": "provider",
|
|
146
|
+
"type": "text",
|
|
147
|
+
"primaryKey": false,
|
|
148
|
+
"notNull": false,
|
|
149
|
+
"autoincrement": false
|
|
150
|
+
},
|
|
151
|
+
"model": {
|
|
152
|
+
"name": "model",
|
|
153
|
+
"type": "text",
|
|
154
|
+
"primaryKey": false,
|
|
155
|
+
"notNull": false,
|
|
156
|
+
"autoincrement": false
|
|
157
|
+
},
|
|
158
|
+
"started_at": {
|
|
159
|
+
"name": "started_at",
|
|
160
|
+
"type": "text",
|
|
161
|
+
"primaryKey": false,
|
|
162
|
+
"notNull": false,
|
|
163
|
+
"autoincrement": false
|
|
164
|
+
},
|
|
165
|
+
"ended_at": {
|
|
166
|
+
"name": "ended_at",
|
|
167
|
+
"type": "text",
|
|
168
|
+
"primaryKey": false,
|
|
169
|
+
"notNull": false,
|
|
170
|
+
"autoincrement": false
|
|
171
|
+
},
|
|
172
|
+
"verdict": {
|
|
173
|
+
"name": "verdict",
|
|
174
|
+
"type": "text",
|
|
175
|
+
"primaryKey": false,
|
|
176
|
+
"notNull": false,
|
|
177
|
+
"autoincrement": false
|
|
178
|
+
},
|
|
179
|
+
"pr_url": {
|
|
180
|
+
"name": "pr_url",
|
|
181
|
+
"type": "text",
|
|
182
|
+
"primaryKey": false,
|
|
183
|
+
"notNull": false,
|
|
184
|
+
"autoincrement": false
|
|
185
|
+
},
|
|
186
|
+
"pr_draft": {
|
|
187
|
+
"name": "pr_draft",
|
|
188
|
+
"type": "integer",
|
|
189
|
+
"primaryKey": false,
|
|
190
|
+
"notNull": false,
|
|
191
|
+
"autoincrement": false
|
|
192
|
+
},
|
|
193
|
+
"cost_usd": {
|
|
194
|
+
"name": "cost_usd",
|
|
195
|
+
"type": "real",
|
|
196
|
+
"primaryKey": false,
|
|
197
|
+
"notNull": false,
|
|
198
|
+
"autoincrement": false
|
|
199
|
+
},
|
|
200
|
+
"partial_usd": {
|
|
201
|
+
"name": "partial_usd",
|
|
202
|
+
"type": "real",
|
|
203
|
+
"primaryKey": false,
|
|
204
|
+
"notNull": true,
|
|
205
|
+
"autoincrement": false
|
|
206
|
+
},
|
|
207
|
+
"measured_sessions": {
|
|
208
|
+
"name": "measured_sessions",
|
|
209
|
+
"type": "integer",
|
|
210
|
+
"primaryKey": false,
|
|
211
|
+
"notNull": true,
|
|
212
|
+
"autoincrement": false
|
|
213
|
+
},
|
|
214
|
+
"total_sessions": {
|
|
215
|
+
"name": "total_sessions",
|
|
216
|
+
"type": "integer",
|
|
217
|
+
"primaryKey": false,
|
|
218
|
+
"notNull": true,
|
|
219
|
+
"autoincrement": false
|
|
220
|
+
},
|
|
221
|
+
"input_tokens": {
|
|
222
|
+
"name": "input_tokens",
|
|
223
|
+
"type": "integer",
|
|
224
|
+
"primaryKey": false,
|
|
225
|
+
"notNull": true,
|
|
226
|
+
"autoincrement": false
|
|
227
|
+
},
|
|
228
|
+
"output_tokens": {
|
|
229
|
+
"name": "output_tokens",
|
|
230
|
+
"type": "integer",
|
|
231
|
+
"primaryKey": false,
|
|
232
|
+
"notNull": true,
|
|
233
|
+
"autoincrement": false
|
|
234
|
+
},
|
|
235
|
+
"wall_s": {
|
|
236
|
+
"name": "wall_s",
|
|
237
|
+
"type": "real",
|
|
238
|
+
"primaryKey": false,
|
|
239
|
+
"notNull": false,
|
|
240
|
+
"autoincrement": false
|
|
241
|
+
},
|
|
242
|
+
"last_seq": {
|
|
243
|
+
"name": "last_seq",
|
|
244
|
+
"type": "integer",
|
|
245
|
+
"primaryKey": false,
|
|
246
|
+
"notNull": true,
|
|
247
|
+
"autoincrement": false
|
|
248
|
+
},
|
|
249
|
+
"last_event_at": {
|
|
250
|
+
"name": "last_event_at",
|
|
251
|
+
"type": "text",
|
|
252
|
+
"primaryKey": false,
|
|
253
|
+
"notNull": false,
|
|
254
|
+
"autoincrement": false
|
|
255
|
+
},
|
|
256
|
+
"tampered": {
|
|
257
|
+
"name": "tampered",
|
|
258
|
+
"type": "integer",
|
|
259
|
+
"primaryKey": false,
|
|
260
|
+
"notNull": true,
|
|
261
|
+
"autoincrement": false
|
|
262
|
+
},
|
|
263
|
+
"conflict": {
|
|
264
|
+
"name": "conflict",
|
|
265
|
+
"type": "integer",
|
|
266
|
+
"primaryKey": false,
|
|
267
|
+
"notNull": true,
|
|
268
|
+
"autoincrement": false,
|
|
269
|
+
"default": 0
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"indexes": {
|
|
273
|
+
"runs_started": {
|
|
274
|
+
"name": "runs_started",
|
|
275
|
+
"columns": [
|
|
276
|
+
"started_at"
|
|
277
|
+
],
|
|
278
|
+
"isUnique": false
|
|
279
|
+
},
|
|
280
|
+
"runs_verdict": {
|
|
281
|
+
"name": "runs_verdict",
|
|
282
|
+
"columns": [
|
|
283
|
+
"verdict"
|
|
284
|
+
],
|
|
285
|
+
"isUnique": false
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"foreignKeys": {},
|
|
289
|
+
"compositePrimaryKeys": {
|
|
290
|
+
"runs_source_id_run_id_pk": {
|
|
291
|
+
"columns": [
|
|
292
|
+
"source_id",
|
|
293
|
+
"run_id"
|
|
294
|
+
],
|
|
295
|
+
"name": "runs_source_id_run_id_pk"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"uniqueConstraints": {},
|
|
299
|
+
"checkConstraints": {}
|
|
300
|
+
},
|
|
301
|
+
"sources": {
|
|
302
|
+
"name": "sources",
|
|
303
|
+
"columns": {
|
|
304
|
+
"id": {
|
|
305
|
+
"name": "id",
|
|
306
|
+
"type": "text",
|
|
307
|
+
"primaryKey": true,
|
|
308
|
+
"notNull": true,
|
|
309
|
+
"autoincrement": false
|
|
310
|
+
},
|
|
311
|
+
"first_seen": {
|
|
312
|
+
"name": "first_seen",
|
|
313
|
+
"type": "text",
|
|
314
|
+
"primaryKey": false,
|
|
315
|
+
"notNull": true,
|
|
316
|
+
"autoincrement": false
|
|
317
|
+
},
|
|
318
|
+
"last_seen": {
|
|
319
|
+
"name": "last_seen",
|
|
320
|
+
"type": "text",
|
|
321
|
+
"primaryKey": false,
|
|
322
|
+
"notNull": true,
|
|
323
|
+
"autoincrement": false
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
"indexes": {},
|
|
327
|
+
"foreignKeys": {},
|
|
328
|
+
"compositePrimaryKeys": {},
|
|
329
|
+
"uniqueConstraints": {},
|
|
330
|
+
"checkConstraints": {}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
"views": {},
|
|
334
|
+
"enums": {},
|
|
335
|
+
"_meta": {
|
|
336
|
+
"schemas": {},
|
|
337
|
+
"tables": {},
|
|
338
|
+
"columns": {}
|
|
339
|
+
},
|
|
340
|
+
"internal": {
|
|
341
|
+
"indexes": {}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loki/control-plane",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "bun test ./test/",
|
|
7
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
8
|
+
"build:ui": "cd ui && bun install --frozen-lockfile && bun run build",
|
|
9
|
+
"build:server": "bun build src/server/serve.ts --target=bun --outfile dist/server.js",
|
|
10
|
+
"build:all": "bun run build:ui && bun run build:server",
|
|
11
|
+
"db:generate": "drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts --out ./drizzle"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"drizzle-orm": "^0.45.3",
|
|
15
|
+
"hono": "^4.13.12"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"drizzle-kit": "^0.31.11"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import * as schema from "./schema.ts";
|
|
7
|
+
|
|
8
|
+
export type Db = ReturnType<typeof openDb>["db"];
|
|
9
|
+
|
|
10
|
+
/** Opens the SQLite file (":memory:" for tests) and applies drizzle/ migrations. Runs on boot. */
|
|
11
|
+
export function openDb(path: string) {
|
|
12
|
+
const sqlite = new Database(path, { create: true });
|
|
13
|
+
sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
|
|
14
|
+
const db = drizzle(sqlite, { schema });
|
|
15
|
+
// src/db/ (source) and dist/ (bundled server) sit at different depths under packages/control-plane
|
|
16
|
+
const migrationsFolder = [join(import.meta.dir, "../../drizzle"), join(import.meta.dir, "../drizzle")].find((d) => existsSync(join(d, "meta/_journal.json")));
|
|
17
|
+
if (!migrationsFolder) throw new Error("drizzle migrations folder not found");
|
|
18
|
+
migrate(db, { migrationsFolder });
|
|
19
|
+
return { db, sqlite };
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { index, integer, primaryKey, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
|
+
|
|
3
|
+
export const sources = sqliteTable("sources", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
firstSeen: text("first_seen").notNull(),
|
|
6
|
+
lastSeen: text("last_seen").notNull(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const events = sqliteTable("events", {
|
|
10
|
+
sourceId: text("source_id").notNull(),
|
|
11
|
+
runId: text("run_id").notNull(),
|
|
12
|
+
seq: integer("seq").notNull(),
|
|
13
|
+
ts: text("ts").notNull(),
|
|
14
|
+
type: text("type").notNull(),
|
|
15
|
+
stage: text("stage"),
|
|
16
|
+
data: text("data", { mode: "json" }).notNull(),
|
|
17
|
+
lineSha256: text("line_sha256").notNull(),
|
|
18
|
+
receivedAt: text("received_at").notNull(),
|
|
19
|
+
}, (t) => [
|
|
20
|
+
primaryKey({ columns: [t.sourceId, t.runId, t.seq] }),
|
|
21
|
+
index("events_run_seq").on(t.runId, t.seq),
|
|
22
|
+
index("events_type_ts").on(t.type, t.ts),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// A projection rebuilt from `events` by fold(); never written any other way.
|
|
26
|
+
export const runs = sqliteTable("runs", {
|
|
27
|
+
sourceId: text("source_id").notNull(),
|
|
28
|
+
runId: text("run_id").notNull(),
|
|
29
|
+
originRepo: text("origin_repo"),
|
|
30
|
+
issueRef: text("issue_ref"),
|
|
31
|
+
taskSource: text("task_source"),
|
|
32
|
+
provider: text("provider"),
|
|
33
|
+
model: text("model"),
|
|
34
|
+
startedAt: text("started_at"),
|
|
35
|
+
endedAt: text("ended_at"),
|
|
36
|
+
verdict: text("verdict"),
|
|
37
|
+
prUrl: text("pr_url"),
|
|
38
|
+
prDraft: integer("pr_draft"),
|
|
39
|
+
costUsd: real("cost_usd"),
|
|
40
|
+
partialUsd: real("partial_usd").notNull(),
|
|
41
|
+
measuredSessions: integer("measured_sessions").notNull(),
|
|
42
|
+
totalSessions: integer("total_sessions").notNull(),
|
|
43
|
+
inputTokens: integer("input_tokens").notNull(),
|
|
44
|
+
outputTokens: integer("output_tokens").notNull(),
|
|
45
|
+
wallS: real("wall_s"),
|
|
46
|
+
lastSeq: integer("last_seq").notNull(),
|
|
47
|
+
lastEventAt: text("last_event_at"),
|
|
48
|
+
tampered: integer("tampered").notNull(),
|
|
49
|
+
conflict: integer("conflict").notNull().default(0),
|
|
50
|
+
}, (t) => [
|
|
51
|
+
primaryKey({ columns: [t.sourceId, t.runId] }),
|
|
52
|
+
index("runs_started").on(t.startedAt),
|
|
53
|
+
index("runs_verdict").on(t.verdict),
|
|
54
|
+
]);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { openDb } from "../db/migrate.ts";
|
|
5
|
+
import { ingest } from "./ingest.ts";
|
|
6
|
+
import { listRuns, runDetail } from "./runs.ts";
|
|
7
|
+
|
|
8
|
+
const MAX_BODY = 1_000_000;
|
|
9
|
+
|
|
10
|
+
/** The built UI: packages/control-plane/ui/dist, found from src/server (source run) or dist (bundled server). */
|
|
11
|
+
const defaultUiDir = () => [join(import.meta.dir, "../../ui/dist"), join(import.meta.dir, "../ui/dist")].find((d) => existsSync(join(d, "index.html")));
|
|
12
|
+
|
|
13
|
+
/** dbPath ":memory:" for tests. Migrations run here, so /ready is true as soon as this returns. uiDir overrides the built-UI location. */
|
|
14
|
+
export function createApp(opts: { dbPath: string; uiDir?: string }) {
|
|
15
|
+
const uiDir = opts.uiDir ?? defaultUiDir();
|
|
16
|
+
const { db, sqlite } = openDb(opts.dbPath);
|
|
17
|
+
let ready = true;
|
|
18
|
+
const app = new Hono();
|
|
19
|
+
|
|
20
|
+
app.get("/health", (c) => c.json({ service: "loki-control", pid: process.pid, install_path: import.meta.dir }));
|
|
21
|
+
app.get("/ready", (c) => {
|
|
22
|
+
try { sqlite.query("select 1").get(); } catch { ready = false; }
|
|
23
|
+
return ready ? c.json({ ready: true }) : c.json({ ready: false }, 503);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.post("/v1/ingest", async (c) => {
|
|
27
|
+
const text = await c.req.text();
|
|
28
|
+
if (text.length > MAX_BODY) return c.json({ error: "body over 1 MB" }, 413);
|
|
29
|
+
let body: unknown;
|
|
30
|
+
try { body = JSON.parse(text); } catch { return c.json({ error: "invalid JSON" }, 400); }
|
|
31
|
+
const r = ingest(db, body);
|
|
32
|
+
return c.json(r.body, r.status);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.get("/v1/runs", (c) => {
|
|
36
|
+
const q = c.req.query();
|
|
37
|
+
return c.json(listRuns(db, { verdict: q.verdict, repo: q.repo, since: q.since, until: q.until, limit: q.limit ? Number(q.limit) : undefined, cursor: q.cursor }));
|
|
38
|
+
});
|
|
39
|
+
const detail = (c: { json: (b: unknown, s?: 404) => Response }, source: string, run: string) => {
|
|
40
|
+
const d = runDetail(db, source, run);
|
|
41
|
+
return d ? c.json(d) : c.json({ error: "run not found" }, 404);
|
|
42
|
+
};
|
|
43
|
+
app.get("/v1/runs/:source/:run", (c) => detail(c, c.req.param("source"), c.req.param("run")));
|
|
44
|
+
// :id is `source:run` (run ids never contain a colon)
|
|
45
|
+
app.get("/v1/runs/:id", (c) => {
|
|
46
|
+
const id = c.req.param("id");
|
|
47
|
+
const i = id.indexOf(":");
|
|
48
|
+
return i < 0 ? c.json({ error: "id must be source:run" }, 404) : detail(c, id.slice(0, i), id.slice(i + 1));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Static UI with SPA fallback. Registered last so /v1, /health and /ready always win.
|
|
52
|
+
app.get("*", (c) => {
|
|
53
|
+
const p = decodeURIComponent(new URL(c.req.url).pathname);
|
|
54
|
+
if (!uiDir || p.startsWith("/v1/")) return c.json({ error: "not found" }, 404);
|
|
55
|
+
const root = resolve(uiDir);
|
|
56
|
+
const f = resolve(root, `.${p}`);
|
|
57
|
+
if (f.startsWith(root + sep) && existsSync(f) && statSync(f).isFile()) return new Response(Bun.file(f));
|
|
58
|
+
if (/\.[a-z0-9]+$/i.test(p)) return c.json({ error: "not found" }, 404); // a missing asset is a 404, not index.html
|
|
59
|
+
return new Response(Bun.file(join(root, "index.html")));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { app, db, close: () => sqlite.close() };
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { validateEnvelope } from "../../../../loki-ts/src/engine10/events.ts";
|
|
4
|
+
import type { EventEnvelope } from "../../../../loki-ts/src/engine10/types.ts";
|
|
5
|
+
import { redactSecrets } from "../../../../loki-ts/src/util/redact.ts";
|
|
6
|
+
import type { Db } from "../db/migrate.ts";
|
|
7
|
+
import { events, runs, sources } from "../db/schema.ts";
|
|
8
|
+
import { rebuildRun } from "./runs.ts";
|
|
9
|
+
|
|
10
|
+
export const MAX_EVENTS = 500;
|
|
11
|
+
|
|
12
|
+
export type IngestResult =
|
|
13
|
+
| { status: 200; body: { accepted: number; duplicate: number; conflict: number; last_seq: number } }
|
|
14
|
+
| { status: 400; body: { error: string } }
|
|
15
|
+
| { status: 409; body: { accepted: number; duplicate: number; conflict: number; last_seq: number; error: string } };
|
|
16
|
+
|
|
17
|
+
/** Body: {source, run_id, events: EventEnvelope[]}. Event id = source:run:seq; same id and same hash is a no-op, a different hash is a 409 that flags the run and never overwrites. */
|
|
18
|
+
export function ingest(db: Db, body: unknown): IngestResult {
|
|
19
|
+
const b = body as { source?: unknown; source_id?: unknown; run_id?: unknown; events?: unknown } | null;
|
|
20
|
+
const sourceId = b?.source ?? b?.source_id;
|
|
21
|
+
if (typeof sourceId !== "string" || !sourceId || typeof b?.run_id !== "string" || !b.run_id) return { status: 400, body: { error: "source and run_id required" } };
|
|
22
|
+
const runId = b.run_id;
|
|
23
|
+
if (!Array.isArray(b.events) || b.events.length > MAX_EVENTS) return { status: 400, body: { error: `events must be an array of at most ${MAX_EVENTS}` } };
|
|
24
|
+
|
|
25
|
+
const rows: { e: EventEnvelope; json: string; sha: string }[] = [];
|
|
26
|
+
for (const [i, raw] of b.events.entries()) {
|
|
27
|
+
const bad = validateEnvelope(raw);
|
|
28
|
+
if (bad) return { status: 400, body: { error: `events[${i}]: ${bad}` } };
|
|
29
|
+
const e = JSON.parse(redactSecrets(JSON.stringify(raw))) as EventEnvelope; // redacted again server-side
|
|
30
|
+
if (e.run !== runId) return { status: 400, body: { error: `events[${i}]: run does not match run_id` } };
|
|
31
|
+
const json = JSON.stringify(e);
|
|
32
|
+
rows.push({ e, json, sha: createHash("sha256").update(json).digest("hex") });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
let accepted = 0, duplicate = 0, conflict = 0;
|
|
37
|
+
db.transaction((tx) => {
|
|
38
|
+
tx.insert(sources).values({ id: sourceId, firstSeen: now, lastSeen: now }).onConflictDoUpdate({ target: sources.id, set: { lastSeen: now } }).run();
|
|
39
|
+
for (const { e, sha } of rows) {
|
|
40
|
+
const prior = tx.select({ sha: events.lineSha256 }).from(events)
|
|
41
|
+
.where(and(eq(events.sourceId, sourceId), eq(events.runId, runId), eq(events.seq, e.seq))).get();
|
|
42
|
+
if (prior) { if (prior.sha === sha) duplicate++; else conflict++; continue; }
|
|
43
|
+
tx.insert(events).values({ sourceId, runId, seq: e.seq, ts: e.ts, type: e.type, stage: e.stage, data: e.data, lineSha256: sha, receivedAt: now }).run();
|
|
44
|
+
accepted++;
|
|
45
|
+
}
|
|
46
|
+
if (accepted > 0) rebuildRun(tx as unknown as Db, sourceId, runId);
|
|
47
|
+
if (conflict > 0) tx.update(runs).set({ conflict: 1 }).where(and(eq(runs.sourceId, sourceId), eq(runs.runId, runId))).run();
|
|
48
|
+
});
|
|
49
|
+
const last = db.select({ m: sql<number | null>`max(${events.seq})` }).from(events).where(and(eq(events.sourceId, sourceId), eq(events.runId, runId))).get()?.m ?? -1;
|
|
50
|
+
const out = { accepted, duplicate, conflict, last_seq: last };
|
|
51
|
+
return conflict > 0 ? { status: 409, body: { ...out, error: "conflicting content for an existing event id" } } : { status: 200, body: out };
|
|
52
|
+
}
|