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.
Files changed (94) hide show
  1. package/README.md +15 -308
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/loki +34 -1
  5. package/autonomy/telemetry.sh +1 -1
  6. package/bin/loki +5 -5
  7. package/completions/_loki +1 -0
  8. package/completions/loki.bash +2 -2
  9. package/dashboard/__init__.py +1 -1
  10. package/docs/INSTALLATION.md +6 -5
  11. package/docs/v10/BOARD.md +1 -0
  12. package/docs/v10/CONTROL-PLANE.md +102 -0
  13. package/docs/v10/DECISIONS.md +33 -0
  14. package/docs/v10/DEPS.md +16 -0
  15. package/docs/v10/LEGACY-REMOVAL.md +101 -0
  16. package/docs/v10/OPERATING-MODEL.md +1 -1
  17. package/docs/v10/PROGRESS.md +6 -0
  18. package/loki-ts/dist/loki.js +485 -464
  19. package/mcp/__init__.py +1 -1
  20. package/package.json +14 -3
  21. package/packages/control-plane/dist/server.js +5919 -0
  22. package/packages/control-plane/drizzle/0000_new_maelstrom.sql +49 -0
  23. package/packages/control-plane/drizzle/meta/0000_snapshot.json +343 -0
  24. package/packages/control-plane/drizzle/meta/_journal.json +13 -0
  25. package/packages/control-plane/package.json +20 -0
  26. package/packages/control-plane/src/db/migrate.ts +20 -0
  27. package/packages/control-plane/src/db/schema.ts +54 -0
  28. package/packages/control-plane/src/server/app.ts +63 -0
  29. package/packages/control-plane/src/server/ingest.ts +52 -0
  30. package/packages/control-plane/src/server/runs.ts +116 -0
  31. package/packages/control-plane/src/server/serve.ts +15 -0
  32. package/packages/control-plane/src/shipper/backfill.ts +40 -0
  33. package/packages/control-plane/src/shipper/ship.ts +95 -0
  34. package/packages/control-plane/ui/dist/assets/index-B6EiyukW.js +114 -0
  35. package/packages/control-plane/ui/dist/assets/index-BH3LtKcG.css +1 -0
  36. package/packages/control-plane/ui/dist/index.html +13 -0
  37. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  38. package/web-app/dist/assets/{AdminPage-DEZMrOU2.js → AdminPage-CH-bmw4l.js} +1 -1
  39. package/web-app/dist/assets/{Avatar-Dcn23Doz.js → Avatar-Bos-ky_x.js} +1 -1
  40. package/web-app/dist/assets/{Badge-CsV7PB4a.js → Badge-DNJyHg9Y.js} +1 -1
  41. package/web-app/dist/assets/{Button-C8Q1ckXg.js → Button-KqD3Tt-I.js} +1 -1
  42. package/web-app/dist/assets/{CockpitPage-YYX4FOT9.js → CockpitPage-l6oTsRaL.js} +1 -1
  43. package/web-app/dist/assets/{ComparePage-BrA6JGOn.js → ComparePage-DN6uxXko.js} +1 -1
  44. package/web-app/dist/assets/{ErrorBoundary-CA3xFm23.js → ErrorBoundary-CKQhnvU9.js} +1 -1
  45. package/web-app/dist/assets/{EvidenceReceiptPanel-C9151UPM.js → EvidenceReceiptPanel-CzSIqLmy.js} +1 -1
  46. package/web-app/dist/assets/{GitHubIssuesPanel-DBYUDlkc.js → GitHubIssuesPanel-YJUcDezn.js} +1 -1
  47. package/web-app/dist/assets/{GitHubPRsPanel-x-Ug3n6Q.js → GitHubPRsPanel-Bv7xFvJN.js} +1 -1
  48. package/web-app/dist/assets/{HomePage-DHPokDA2.js → HomePage-CJsxBbii.js} +2 -2
  49. package/web-app/dist/assets/{LoginPage-CB8h5M-E.js → LoginPage-D1V2Ag0s.js} +1 -1
  50. package/web-app/dist/assets/{MagicPage-B6Zg4DHE.js → MagicPage-BBVNMEr5.js} +1 -1
  51. package/web-app/dist/assets/{MetricsPage-JN6AkYit.js → MetricsPage-CtHM-IlA.js} +1 -1
  52. package/web-app/dist/assets/{NotFoundPage-dOUCtSdt.js → NotFoundPage-BBEEJLj5.js} +1 -1
  53. package/web-app/dist/assets/{ProjectPage-DfyhdFcg.js → ProjectPage-BVRJoEaQ.js} +3 -3
  54. package/web-app/dist/assets/{ProjectsPage-oHMOXioV.js → ProjectsPage-DqaA9rUa.js} +1 -1
  55. package/web-app/dist/assets/{SettingsPage-D4BGbBbt.js → SettingsPage-B_RQV_vF.js} +1 -1
  56. package/web-app/dist/assets/{ShowcasePage-CCkk1kPK.js → ShowcasePage-DqN-mrZ8.js} +1 -1
  57. package/web-app/dist/assets/{SystemSettingsPage-N2_JdIbZ.js → SystemSettingsPage-D-CvuL-K.js} +1 -1
  58. package/web-app/dist/assets/{TeamsPage-CEDSFaIj.js → TeamsPage-CzkkDcPj.js} +1 -1
  59. package/web-app/dist/assets/{TemplatesPage-DB9WjPjI.js → TemplatesPage-Bbchk7YQ.js} +1 -1
  60. package/web-app/dist/assets/{TerminalOutput-DK55nBdV.js → TerminalOutput-B32BAX--.js} +1 -1
  61. package/web-app/dist/assets/{activity-3EPj3lfE.js → activity-C9vnEmhG.js} +1 -1
  62. package/web-app/dist/assets/{bell-05FwE2g4.js → bell-XsIhkyjq.js} +1 -1
  63. package/web-app/dist/assets/{bot-DAtzuPmj.js → bot-CvQhqBws.js} +1 -1
  64. package/web-app/dist/assets/{check-DVZjl88M.js → check-HmG8WcD-.js} +1 -1
  65. package/web-app/dist/assets/{chevron-left-BaNjYaIh.js → chevron-left-C1KNgnbD.js} +1 -1
  66. package/web-app/dist/assets/{circle-alert-DlvI6QdR.js → circle-alert-DSmCvZju.js} +1 -1
  67. package/web-app/dist/assets/{clock-B0vWVEQz.js → clock-CPhE3RXd.js} +1 -1
  68. package/web-app/dist/assets/{cloud-Du5W0a4Q.js → cloud-29lwqocB.js} +1 -1
  69. package/web-app/dist/assets/{code-xml-CBNJDFTV.js → code-xml-4jZUzbPt.js} +1 -1
  70. package/web-app/dist/assets/{database-CCqFWW7j.js → database-H0Ct2r-8.js} +1 -1
  71. package/web-app/dist/assets/{dollar-sign-BiD3YO1u.js → dollar-sign-DsXkO0yT.js} +1 -1
  72. package/web-app/dist/assets/{file-code-corner-DCelfbWq.js → file-code-corner-BM8w_nvX.js} +1 -1
  73. package/web-app/dist/assets/{file-plus-B18v-CXD.js → file-plus-BWkYTJ4D.js} +1 -1
  74. package/web-app/dist/assets/{globe-DdaeBtBG.js → globe-FOFDl_8Z.js} +1 -1
  75. package/web-app/dist/assets/{hammer-BaxkIYAS.js → hammer-orm5gvky.js} +1 -1
  76. package/web-app/dist/assets/{index-BnsxsCEv.js → index-B6juCuPX.js} +3 -3
  77. package/web-app/dist/assets/{layers-jmRqcTgA.js → layers-DUQPkPAl.js} +1 -1
  78. package/web-app/dist/assets/{loader-circle-B5eWOgN-.js → loader-circle-Deq6bDar.js} +1 -1
  79. package/web-app/dist/assets/{lock-DCOObKS4.js → lock-CSzWTPRC.js} +1 -1
  80. package/web-app/dist/assets/{package-1nzEtHlZ.js → package-CmACgZuF.js} +1 -1
  81. package/web-app/dist/assets/{plus-B5cRKjki.js → plus-BjPW5v3A.js} +1 -1
  82. package/web-app/dist/assets/{refresh-cw-D769PBG8.js → refresh-cw-DhI1sHaj.js} +1 -1
  83. package/web-app/dist/assets/{rotate-ccw-CTHsioqQ.js → rotate-ccw-Byi2ouWB.js} +1 -1
  84. package/web-app/dist/assets/{scroll-text-DcwZwpH-.js → scroll-text-gq7bJ81H.js} +1 -1
  85. package/web-app/dist/assets/{server-DvIdr3uc.js → server-YbY5s1-X.js} +1 -1
  86. package/web-app/dist/assets/{shield-alert-HsBQ7gvc.js → shield-alert-DhKjtwZt.js} +1 -1
  87. package/web-app/dist/assets/{thumbs-up-Dwy2c2yM.js → thumbs-up-D2X5JR-5.js} +1 -1
  88. package/web-app/dist/assets/{trash-2-DhZM5W4J.js → trash-2-BHAmMxTq.js} +1 -1
  89. package/web-app/dist/assets/{trending-down-Bw3nDTCd.js → trending-down-C41kQa-Q.js} +1 -1
  90. package/web-app/dist/assets/{trending-up-CbZDPnht.js → trending-up-M-OK_9DC.js} +1 -1
  91. package/web-app/dist/assets/{upload-w6RYv_bT.js → upload-CarpPiGw.js} +1 -1
  92. package/web-app/dist/assets/{usePolling-9hkUOMWP.js → usePolling-rJ7rM7R0.js} +1 -1
  93. package/web-app/dist/assets/{user-Cct2X0U_.js → user-cBhWpZen.js} +1 -1
  94. 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,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1790870373167,
9
+ "tag": "0000_new_maelstrom",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -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
+ }