turbollm 1.9.2 → 1.9.4

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 (112) hide show
  1. package/README.md +25 -6
  2. package/dist/cli.js +863 -223
  3. package/dist/webdist/assets/{AgentEditPage-CyzZ8vDr.js → AgentEditPage-CbJqYCVo.js} +1 -1
  4. package/dist/webdist/assets/{ChatScreen-DWVblPWw.js → ChatScreen-LaTz7fTZ.js} +3 -3
  5. package/dist/webdist/assets/CodeComposer-12z9dbZA.js +1 -0
  6. package/dist/webdist/assets/CodeHomeScreen-8A3Anu2L.js +1 -0
  7. package/dist/webdist/assets/{CodeSessionScreen-BtA5u3li.js → CodeSessionScreen-BoA5rPp7.js} +3 -3
  8. package/dist/webdist/assets/CustomizeScreen-BTBhphFE.js +4 -0
  9. package/dist/webdist/assets/DeveloperScreen-Dfy6Rgw4.js +1 -0
  10. package/dist/webdist/assets/{EnginesScreen-ChGdOOW9.js → EnginesScreen-Dwxe8B5p.js} +6 -6
  11. package/dist/webdist/assets/FsBrowser-DXxO1QdQ.js +1 -0
  12. package/dist/webdist/assets/{ModelLoadMenu-DzaxKmIV.js → ModelLoadMenu-C7wK_COK.js} +2 -2
  13. package/dist/webdist/assets/{ModelsScreen-wuI7h511.js → ModelsScreen-DnpYzOj2.js} +2 -2
  14. package/dist/webdist/assets/SettingsScreen-Tn8PlYcS.js +2 -0
  15. package/dist/webdist/assets/{SkillEditPage-XGTaeQ8o.js → SkillEditPage-Bzr1Mfqi.js} +1 -1
  16. package/dist/webdist/assets/{TokensScreen-B02y3G-H.js → TokensScreen-BrnFne8W.js} +1 -1
  17. package/dist/webdist/assets/{ToolApprovalBar-D4YRUMat.js → ToolApprovalBar-BfIwjh_G.js} +4 -4
  18. package/dist/webdist/assets/WorkspaceScreen-CHg7GC1T.js +1 -0
  19. package/dist/webdist/assets/{agent-api-D86F96i6.js → agent-api-C0qMHAE0.js} +1 -1
  20. package/dist/webdist/assets/{arc-BScxM0vj.js → arc-DGmQFsDA.js} +1 -1
  21. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-DbIjWGrc.js → architectureDiagram-3BPJPVTR-DSypGjGj.js} +1 -1
  22. package/dist/webdist/assets/{arrow-left-Cz7XgLgo.js → arrow-left-CuK9vFgW.js} +1 -1
  23. package/dist/webdist/assets/{badge-CRVn2top.js → badge-jlftBi-Q.js} +1 -1
  24. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-COy0nYNX.js → blockDiagram-GPEHLZMM-DoYsQhuo.js} +1 -1
  25. package/dist/webdist/assets/{bot-C-sGABri.js → bot-CE50aKt3.js} +1 -1
  26. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-CoiZua-v.js → c4Diagram-AAUBKEIU-DsQ4mGev.js} +1 -1
  27. package/dist/webdist/assets/channel-BBYa3XlX.js +1 -0
  28. package/dist/webdist/assets/{chat-api-CdVOTSst.js → chat-api-BQgsmVCJ.js} +1 -1
  29. package/dist/webdist/assets/{chat-queries-DINfS8uv.js → chat-queries-BuU4cQ0I.js} +1 -1
  30. package/dist/webdist/assets/{chevron-left-CbPqt11D.js → chevron-left-BoIfI017.js} +1 -1
  31. package/dist/webdist/assets/{chevron-right-C8EPnhrg.js → chevron-right-Btn5ue2K.js} +1 -1
  32. package/dist/webdist/assets/{chunk-2J33WTMH-B-ggs358.js → chunk-2J33WTMH-Cpm4yaej.js} +1 -1
  33. package/dist/webdist/assets/{chunk-4BX2VUAB-CBSHARpe.js → chunk-4BX2VUAB-Qkt8VpMd.js} +1 -1
  34. package/dist/webdist/assets/{chunk-55IACEB6-BikCaTgS.js → chunk-55IACEB6-D0lIZ5zv.js} +1 -1
  35. package/dist/webdist/assets/{chunk-727SXJPM-B8Uooh_X.js → chunk-727SXJPM-DmJbKG_f.js} +1 -1
  36. package/dist/webdist/assets/{chunk-AQP2D5EJ-C-wZiAmx.js → chunk-AQP2D5EJ-CP5FXopV.js} +1 -1
  37. package/dist/webdist/assets/{chunk-FMBD7UC4-lkRMU7V5.js → chunk-FMBD7UC4-CEHj-mOu.js} +1 -1
  38. package/dist/webdist/assets/{chunk-ND2GUHAM-CTBUDTXK.js → chunk-ND2GUHAM-DfaWkbDp.js} +1 -1
  39. package/dist/webdist/assets/{chunk-QZHKN3VN-CKOXFQ-c.js → chunk-QZHKN3VN-Cbt4eIAq.js} +1 -1
  40. package/dist/webdist/assets/{circle-check-dbo4u23c.js → circle-check-CyJ0DXgY.js} +1 -1
  41. package/dist/webdist/assets/{circle-x-CuPWHB4Y.js → circle-x-B2ihWKOZ.js} +1 -1
  42. package/dist/webdist/assets/classDiagram-4FO5ZUOK-3anTq5dR.js +1 -0
  43. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-3anTq5dR.js +1 -0
  44. package/dist/webdist/assets/{common-RZkQjs09.js → common-CA-hju85.js} +1 -1
  45. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-L3BPlYev.js → cose-bilkent-S5V4N54A-U7v4kFOg.js} +1 -1
  46. package/dist/webdist/assets/{dagre-BM42HDAG-DdFZFhxI.js → dagre-BM42HDAG-BrRqvg5R.js} +1 -1
  47. package/dist/webdist/assets/{diagram-2AECGRRQ-Bx4JyVtd.js → diagram-2AECGRRQ-CSOuZx8C.js} +1 -1
  48. package/dist/webdist/assets/{diagram-5GNKFQAL-DYxtCUYO.js → diagram-5GNKFQAL-Dc9npHHs.js} +1 -1
  49. package/dist/webdist/assets/{diagram-KO2AKTUF-BSV0zTVR.js → diagram-KO2AKTUF-CRMmrYOC.js} +1 -1
  50. package/dist/webdist/assets/{diagram-LMA3HP47-CS-L_g7_.js → diagram-LMA3HP47-DR-qZazu.js} +1 -1
  51. package/dist/webdist/assets/{diagram-OG6HWLK6-D8uK9mOu.js → diagram-OG6HWLK6-BRt3haop.js} +1 -1
  52. package/dist/webdist/assets/{dialog-fsfhY_Ib.js → dialog-OPLOEGn-.js} +1 -1
  53. package/dist/webdist/assets/{dropdown-menu-B6pYEdad.js → dropdown-menu-mOPaoWK5.js} +1 -1
  54. package/dist/webdist/assets/{erDiagram-TEJ5UH35-DX-3blsY.js → erDiagram-TEJ5UH35-D0M-EUsh.js} +1 -1
  55. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-DUTsLqgt.js → flowDiagram-I6XJVG4X-DBufQOmB.js} +1 -1
  56. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-CwH0oq0S.js → ganttDiagram-6RSMTGT7-B6zSUp2B.js} +1 -1
  57. package/dist/webdist/assets/{git-branch-BiMfcJ1j.js → git-branch-D8GYPzhY.js} +1 -1
  58. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-CmLeqvWh.js → gitGraphDiagram-PVQCEYII-CnSmYO2G.js} +1 -1
  59. package/dist/webdist/assets/index-COUI_CR-.js +13 -0
  60. package/dist/webdist/assets/index-DKAl71q8.css +1 -0
  61. package/dist/webdist/assets/{infoDiagram-5YYISTIA-BFTUzkbq.js → infoDiagram-5YYISTIA-F6nF57e3.js} +1 -1
  62. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-BdQIrqQU.js → ishikawaDiagram-YF4QCWOH-DXMvubwx.js} +1 -1
  63. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-D1HmHaNc.js → journeyDiagram-JHISSGLW-BcMZWZZi.js} +1 -1
  64. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-C5USgc8m.js → kanban-definition-UN3LZRKU-BhA3iHrI.js} +1 -1
  65. package/dist/webdist/assets/{linear-BDGxyxLo.js → linear-DCHP6NLs.js} +1 -1
  66. package/dist/webdist/assets/{lock-DCb387O_.js → lock-DEL9SW4n.js} +1 -1
  67. package/dist/webdist/assets/{mermaid.core-DWCJxsCl.js → mermaid.core-Bvxj3QV6.js} +4 -4
  68. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-BV3H5rWG.js → mindmap-definition-RKZ34NQL-DFW-B7At.js} +1 -1
  69. package/dist/webdist/assets/{pencil-BegDY0Zk.js → pencil-D3YoSpTL.js} +1 -1
  70. package/dist/webdist/assets/{personas-DNCENHUd.js → personas-BL5LWber.js} +2 -2
  71. package/dist/webdist/assets/{pieDiagram-4H26LBE5-DDN9VAOL.js → pieDiagram-4H26LBE5-D22oz4Bh.js} +1 -1
  72. package/dist/webdist/assets/{plus-DrZmKBTt.js → plus-Czof1-k0.js} +1 -1
  73. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-Bx16Ru8w.js → quadrantDiagram-W4KKPZXB-rH5_lBaU.js} +1 -1
  74. package/dist/webdist/assets/{refresh-cw-B7Qjdj47.js → refresh-cw-eHr6ZrHD.js} +1 -1
  75. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-k0upGaul.js → requirementDiagram-4Y6WPE33-2GvaIJsP.js} +1 -1
  76. package/dist/webdist/assets/{rotate-ccw-B7o8hLk6.js → rotate-ccw-DBvaE0Sq.js} +1 -1
  77. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-xyOWBpSr.js → sankeyDiagram-5OEKKPKP-VB7s7E26.js} +1 -1
  78. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-BhBclCRW.js → sequenceDiagram-3UESZ5HK-DypBxMgR.js} +1 -1
  79. package/dist/webdist/assets/{skeleton-D15eL_7e.js → skeleton-COip0Uxa.js} +1 -1
  80. package/dist/webdist/assets/{sliders-horizontal-C4_t3PES.js → sliders-horizontal-BYbWJESN.js} +1 -1
  81. package/dist/webdist/assets/{sparkles-BT45JGMR.js → sparkles-Bunle1wO.js} +1 -1
  82. package/dist/webdist/assets/{star-SskkfhIb.js → star-CDjPloh2.js} +1 -1
  83. package/dist/webdist/assets/{stateDiagram-AJRCARHV-902Ow1i4.js → stateDiagram-AJRCARHV-De6Tx6UP.js} +1 -1
  84. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BJ8zIW-S.js +1 -0
  85. package/dist/webdist/assets/{switch-C-l-ej5M.js → switch-CrtIhLpg.js} +1 -1
  86. package/dist/webdist/assets/{terminal-55Og1Fyx.js → terminal-C_jrBBqy.js} +1 -1
  87. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-BEyTzrTZ.js → timeline-definition-PNZ67QCA-DzMJnztZ.js} +1 -1
  88. package/dist/webdist/assets/{trash-2-B9nMCHMA.js → trash-2-BA8fBFXw.js} +1 -1
  89. package/dist/webdist/assets/useIsDesktop-Dg2G692v.js +1 -0
  90. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-C77U6Nzk.js → vennDiagram-CIIHVFJN-BNR_zHHW.js} +1 -1
  91. package/dist/webdist/assets/{wardley-L42UT6IY-BZ10FiAs.js → wardley-L42UT6IY-Cm1Sh5Oj.js} +1 -1
  92. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-DgrAaqyf.js → wardleyDiagram-YWT4CUSO-Bvni8f-6.js} +1 -1
  93. package/dist/webdist/assets/{wrench-DMY_WSQ9.js → wrench-Uhws4miP.js} +1 -1
  94. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BlzVJUn3.js → xychartDiagram-2RQKCTM6-Dyk7o391.js} +1 -1
  95. package/dist/webdist/index.html +2 -2
  96. package/package.json +1 -1
  97. package/dist/webdist/assets/CodeComposer-BvNMY6PI.js +0 -1
  98. package/dist/webdist/assets/CodeHomeScreen-CmzaqOmH.js +0 -1
  99. package/dist/webdist/assets/CustomizeScreen-ACC_4SMn.js +0 -4
  100. package/dist/webdist/assets/DeveloperScreen-CSJZh91v.js +0 -1
  101. package/dist/webdist/assets/FsBrowser-D9RjrjvO.js +0 -1
  102. package/dist/webdist/assets/SettingsScreen-DsKi9vxM.js +0 -1
  103. package/dist/webdist/assets/WorkspaceScreen-DRoUxluw.js +0 -1
  104. package/dist/webdist/assets/channel-8eXCTkKl.js +0 -1
  105. package/dist/webdist/assets/check-Do9IgpFj.js +0 -1
  106. package/dist/webdist/assets/classDiagram-4FO5ZUOK-D5bPDMhb.js +0 -1
  107. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-D5bPDMhb.js +0 -1
  108. package/dist/webdist/assets/copy-button-DKUAZ6Ig.js +0 -1
  109. package/dist/webdist/assets/index-CXwfn7-U.css +0 -1
  110. package/dist/webdist/assets/index-DaNuMYPC.js +0 -10
  111. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-B5LKF3CJ.js +0 -1
  112. package/dist/webdist/assets/useIsDesktop-D2KX8NOA.js +0 -1
package/dist/cli.js CHANGED
@@ -23,9 +23,9 @@ function hideChildConsoleWindows() {
23
23
 
24
24
  // src/cli.ts
25
25
  import { spawn as spawn8 } from "child_process";
26
- import { openSync as openSync3, readFileSync as readFileSync16 } from "fs";
26
+ import { openSync as openSync3, readFileSync as readFileSync19 } from "fs";
27
27
  import { serve } from "@hono/node-server";
28
- import { dirname as dirname12, join as join33 } from "path";
28
+ import { dirname as dirname12, join as join35 } from "path";
29
29
  import { fileURLToPath as fileURLToPath2 } from "url";
30
30
 
31
31
  // src/config/config.ts
@@ -33,7 +33,7 @@ import { randomUUID } from "crypto";
33
33
  import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
34
34
  import { homedir } from "os";
35
35
  import { dirname, join } from "path";
36
- var SCHEMA_VERSION = 3;
36
+ var SCHEMA_VERSION = 4;
37
37
  var VRAM_HEADROOM_MIN_MB = 300;
38
38
  var VRAM_HEADROOM_MAX_MB = 2048;
39
39
  var VRAM_HEADROOM_DEFAULT_MB = 1024;
@@ -188,7 +188,7 @@ var ConfigStore = class _ConfigStore {
188
188
  renameSync(tmp, this.filePath);
189
189
  }
190
190
  };
191
- function migrate(raw, _from) {
191
+ function migrate(raw, from) {
192
192
  const cfg2 = defaultConfig();
193
193
  if (typeof raw.host === "string") cfg2.daemon.host = raw.host;
194
194
  if (typeof raw.port === "number") cfg2.daemon.port = raw.port;
@@ -215,6 +215,7 @@ function migrate(raw, _from) {
215
215
  cfg2[k] = v;
216
216
  }
217
217
  }
218
+ if (from < 4 && cfg2.telemetry.level === "off") cfg2.telemetry.level = "unset";
218
219
  return cfg2;
219
220
  }
220
221
  function splitLaunchArgs(args) {
@@ -1404,6 +1405,10 @@ var Manager = class _Manager {
1404
1405
  child = null;
1405
1406
  startedAt = 0;
1406
1407
  errInfo = null;
1408
+ /** Optional observer for load outcomes, wired in cli.ts (ADR-299). Kept as a
1409
+ * plain callback so Manager has no telemetry dependency and tests need no
1410
+ * stub. A throwing observer must never break a load. */
1411
+ onLoadSettled;
1407
1412
  lastCommand = null;
1408
1413
  lastActivity = 0;
1409
1414
  logPathStr = "";
@@ -1427,9 +1432,11 @@ var Manager = class _Manager {
1427
1432
  if (hooks?.beforeStart) await hooks.beforeStart();
1428
1433
  await this.startInternal(opts);
1429
1434
  await this.awaitNotStarting();
1435
+ this.reportLoad(this.state === "running", this.errInfo);
1430
1436
  } catch (e) {
1431
1437
  this.state = "error";
1432
1438
  this.errInfo = { code: "load_failed", message: e instanceof Error ? e.message : String(e), exitCode: -1, logTail: [] };
1439
+ this.reportLoad(false, this.errInfo);
1433
1440
  throw e;
1434
1441
  }
1435
1442
  });
@@ -1444,6 +1451,14 @@ var Manager = class _Manager {
1444
1451
  await this.awaitNotStarting();
1445
1452
  });
1446
1453
  }
1454
+ /** Notify the load observer, swallowing anything it throws — an observer is
1455
+ * by definition not allowed to affect whether a model loaded. */
1456
+ reportLoad(ok, err7) {
1457
+ try {
1458
+ this.onLoadSettled?.(ok, err7);
1459
+ } catch {
1460
+ }
1461
+ }
1447
1462
  /** Wait until the engine leaves the 'starting' state (→ running or error/stopped),
1448
1463
  * bounded by the engine kind's readiness window plus a small grace. The internal
1449
1464
  * readiness loop flips the state and surfaces errors; this just keeps the load
@@ -1475,9 +1490,9 @@ var Manager = class _Manager {
1475
1490
  }
1476
1491
  }
1477
1492
  const port2 = await allocPort(opts.preferredPort);
1478
- const logPath = join10(this.store.dir(), "logs", `engine-${opts.engine.id}.log`);
1479
- mkdirSync6(dirname2(logPath), { recursive: true });
1480
- const logStream = createWriteStream2(logPath);
1493
+ const logPath2 = join10(this.store.dir(), "logs", `engine-${opts.engine.id}.log`);
1494
+ mkdirSync6(dirname2(logPath2), { recursive: true });
1495
+ const logStream = createWriteStream2(logPath2);
1481
1496
  logStream.write(
1482
1497
  `[turbollm] starting engine "${opts.engine.name}" on internal port ${port2} (127.0.0.1 only \u2014 the engine's own port, NOT the TurboLLM app/UI port).
1483
1498
  `
@@ -1507,7 +1522,7 @@ var Manager = class _Manager {
1507
1522
  this.startedAt = Date.now();
1508
1523
  this.errInfo = null;
1509
1524
  this.lastActivity = Date.now();
1510
- this.logPathStr = logPath;
1525
+ this.logPathStr = logPath2;
1511
1526
  this.exited = new Promise((res) => {
1512
1527
  this.resolveExited = res;
1513
1528
  });
@@ -3311,11 +3326,23 @@ var ProvisionState = class {
3311
3326
  this.s.part = part;
3312
3327
  this.s.parts = parts;
3313
3328
  }
3329
+ /** Optional observer for the terminal outcome, wired in cli.ts (ADR-299
3330
+ * `onboarding_step`). The error string is deliberately not passed — the only
3331
+ * consumer may never transmit free text. */
3332
+ onSettled;
3314
3333
  done() {
3315
3334
  this.s = { active: false, phase: "idle", backend: "", pct: 0, part: 1, parts: 1, error: null };
3335
+ this.settle(true);
3316
3336
  }
3317
3337
  fail(error) {
3318
3338
  this.s = { active: false, phase: "error", backend: this.s.backend, pct: 0, part: 1, parts: 1, error };
3339
+ this.settle(false);
3340
+ }
3341
+ settle(ok) {
3342
+ try {
3343
+ this.onSettled?.(ok);
3344
+ } catch {
3345
+ }
3319
3346
  }
3320
3347
  };
3321
3348
 
@@ -3345,11 +3372,24 @@ var BuildState = class {
3345
3372
  this.s.log.push(trimmed);
3346
3373
  if (this.s.log.length > LOG_TAIL) this.s.log.splice(0, this.s.log.length - LOG_TAIL);
3347
3374
  }
3375
+ /** Optional observer for the terminal outcome, wired in cli.ts (ADR-299
3376
+ * `onboarding_step`). A plain callback so this module keeps no telemetry
3377
+ * dependency; the error STRING is deliberately not passed, because the only
3378
+ * consumer may never send free text. */
3379
+ onSettled;
3348
3380
  done() {
3349
3381
  this.s = { ...this.s, active: false, phase: "done", error: null };
3382
+ this.settle(true);
3350
3383
  }
3351
3384
  fail(error) {
3352
3385
  this.s = { ...this.s, active: false, phase: "error", error };
3386
+ this.settle(false);
3387
+ }
3388
+ settle(ok) {
3389
+ try {
3390
+ this.onSettled?.(ok);
3391
+ } catch {
3392
+ }
3353
3393
  }
3354
3394
  };
3355
3395
 
@@ -7004,6 +7044,18 @@ var DownloadManager = class {
7004
7044
  provenancePath;
7005
7045
  provenanceList = [];
7006
7046
  nextSeq = 0;
7047
+ /** Optional observer for a download's terminal outcome, wired in cli.ts
7048
+ * (ADR-299 `onboarding_step`). Separate from `onComplete`, which fires only
7049
+ * on success and exists to trigger a rescan — reusing it would have made a
7050
+ * failed download indistinguishable from one that never started. The error
7051
+ * string is deliberately not passed: the only consumer may never send text. */
7052
+ onSettled;
7053
+ settle(outcome) {
7054
+ try {
7055
+ this.onSettled?.(outcome);
7056
+ } catch {
7057
+ }
7058
+ }
7007
7059
  /** Permanent record of which repo+file each downloaded model came from (spec 10 §3).
7008
7060
  * Consumed by the repo-detail route to mark a quant "Downloaded" for the exact repo
7009
7061
  * it was pulled from. */
@@ -7276,6 +7328,7 @@ var DownloadManager = class {
7276
7328
  this.onComplete();
7277
7329
  } catch {
7278
7330
  }
7331
+ this.settle("ok");
7279
7332
  } catch (e) {
7280
7333
  this.controllers.delete(rec.id);
7281
7334
  rec.bytesPerSec = 0;
@@ -7287,6 +7340,7 @@ var DownloadManager = class {
7287
7340
  rec.error = e instanceof Error ? e.message : String(e);
7288
7341
  }
7289
7342
  this.persist();
7343
+ this.settle(rec.status === "cancelled" ? "cancelled" : "fail");
7290
7344
  } finally {
7291
7345
  this.pump();
7292
7346
  }
@@ -7422,9 +7476,8 @@ function repoSubdir(repo) {
7422
7476
 
7423
7477
  // src/bench/bench.ts
7424
7478
  import { execFile as execFile10 } from "child_process";
7425
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
7426
- import { join as join19 } from "path";
7427
- import { randomUUID as randomUUID4 } from "crypto";
7479
+ import { readFileSync as readFileSync9 } from "fs";
7480
+ import { randomUUID as randomUUID5 } from "crypto";
7428
7481
 
7429
7482
  // src/api/path-utils.ts
7430
7483
  function inferRepoFromPath(filePath, modelDirs) {
@@ -7443,6 +7496,353 @@ function inferRepoFromPath(filePath, modelDirs) {
7443
7496
  return null;
7444
7497
  }
7445
7498
 
7499
+ // src/telemetry/queue.ts
7500
+ import { mkdirSync as mkdirSync10, readdirSync as readdirSync6, readFileSync as readFileSync7, rmSync as rmSync10, writeFileSync as writeFileSync8 } from "fs";
7501
+ import { join as join19 } from "path";
7502
+ import { randomUUID as randomUUID4 } from "crypto";
7503
+
7504
+ // src/telemetry/schema.ts
7505
+ var TELEMETRY_SCHEMA_VERSION = 1;
7506
+ var EVENT_NAMES = [
7507
+ "app_first_run",
7508
+ "daily_active",
7509
+ "onboarding_step",
7510
+ "model_first_load",
7511
+ "feature_first_use",
7512
+ "feature_used_daily",
7513
+ "error",
7514
+ "consent_choice",
7515
+ "bench_result"
7516
+ ];
7517
+ var ENVELOPE_KEYS = ["schema", "event", "ts", "machineId", "app", "hw", "payload", "level"];
7518
+ var FEATURES = [
7519
+ "chat",
7520
+ "code",
7521
+ "research",
7522
+ "artifacts",
7523
+ "mcp",
7524
+ "agents",
7525
+ "autotune",
7526
+ "skills",
7527
+ "image"
7528
+ ];
7529
+ var ONBOARDING_STEPS = ["engine_install", "engine_build", "model_download", "first_load"];
7530
+ var OUTCOMES = ["ok", "fail", "cancelled"];
7531
+ var FAIL_REASONS = [
7532
+ "oom",
7533
+ "no_engine",
7534
+ "bad_gguf",
7535
+ "unsupported_arch",
7536
+ "timeout",
7537
+ "cancelled",
7538
+ "other"
7539
+ ];
7540
+ var COUNT_BUCKETS = ["1", "2-5", "6-20", "21-100", "100+"];
7541
+ var ERROR_FINGERPRINTS = [
7542
+ "cuda_oom",
7543
+ "engine_crash",
7544
+ "engine_start_timeout",
7545
+ "model_load_failed",
7546
+ "gateway_unreachable",
7547
+ "download_failed",
7548
+ "build_failed",
7549
+ "other"
7550
+ ];
7551
+ var CONSENT_LEVELS = ["off", "anon", "full"];
7552
+ var MAX_IDENT_LEN = 96;
7553
+ function isSafeIdent(v) {
7554
+ if (typeof v !== "string" || v.length === 0 || v.length > MAX_IDENT_LEN) return false;
7555
+ if (!/^[A-Za-z0-9 ._:()+\-]+$/.test(v)) return false;
7556
+ if (v.includes("..")) return false;
7557
+ return true;
7558
+ }
7559
+ function isSafeOs(v) {
7560
+ return typeof v === "string" && /^[a-z0-9]{1,32}\/[a-z0-9]{1,32}$/.test(v);
7561
+ }
7562
+ var BENCH_PAYLOAD = {
7563
+ model: {
7564
+ name: { kind: "ident" },
7565
+ quant: { kind: "ident" },
7566
+ arch: { kind: "ident" },
7567
+ sizeBytes: { kind: "number" },
7568
+ moe: { kind: "boolean" }
7569
+ },
7570
+ engine: { version: { kind: "ident" } },
7571
+ params: {
7572
+ ctx: { kind: "number" },
7573
+ ngl: { kind: "number" },
7574
+ nglFit: { kind: "boolean", optional: true },
7575
+ nCpuMoe: { kind: "number" },
7576
+ nCpuMoeFit: { kind: "boolean", optional: true },
7577
+ parallel: { kind: "number" },
7578
+ kvTypeK: { kind: "ident" },
7579
+ flashAttn: { kind: "ident" }
7580
+ },
7581
+ result: {
7582
+ tps: { kind: "number" },
7583
+ ttftMs: { kind: "number" },
7584
+ // `null` is real data here: BenchResult.vramMb is `number | null`, meaning
7585
+ // "we could not measure VRAM on this box" — distinct from "not reported".
7586
+ vramMb: { kind: "number", optional: true, nullable: true },
7587
+ outcome: { enum: OUTCOMES }
7588
+ }
7589
+ };
7590
+ var HW_SPEC = {
7591
+ cpu: { kind: "ident" },
7592
+ ramMb: { kind: "number" }
7593
+ };
7594
+ var PAYLOAD_SPECS = {
7595
+ onboarding_step: { step: { enum: ONBOARDING_STEPS }, outcome: { enum: OUTCOMES } },
7596
+ model_first_load: { outcome: { enum: OUTCOMES }, failReason: { enum: FAIL_REASONS, optional: true } },
7597
+ feature_first_use: { feature: { enum: FEATURES } },
7598
+ feature_used_daily: { feature: { enum: FEATURES }, countBucket: { enum: COUNT_BUCKETS } },
7599
+ error: { fingerprint: { enum: ERROR_FINGERPRINTS } }
7600
+ };
7601
+ function checkFields(obj, spec, path) {
7602
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
7603
+ return `${path} must be an object`;
7604
+ }
7605
+ const o = obj;
7606
+ for (const key of Object.keys(o)) {
7607
+ if (!Object.prototype.hasOwnProperty.call(spec, key)) return `unknown field: ${path}.${key}`;
7608
+ }
7609
+ for (const [key, field] of Object.entries(spec)) {
7610
+ const v = o[key];
7611
+ if (v === void 0) {
7612
+ if (field.optional) continue;
7613
+ return `missing field: ${path}.${key}`;
7614
+ }
7615
+ const where = `${path}.${key}`;
7616
+ if (v === null && !("enum" in field) && field.nullable) continue;
7617
+ if ("enum" in field) {
7618
+ if (typeof v !== "string" || !field.enum.includes(v)) return `invalid value for ${where}: ${String(v)}`;
7619
+ } else if (field.kind === "ident") {
7620
+ if (!isSafeIdent(v)) return `invalid value for ${where}`;
7621
+ } else if (field.kind === "os") {
7622
+ if (!isSafeOs(v)) return `invalid value for ${where}`;
7623
+ } else if (field.kind === "number") {
7624
+ if (typeof v !== "number" || !Number.isFinite(v)) return `invalid value for ${where}: ${String(v)}`;
7625
+ } else if (typeof v !== "boolean") {
7626
+ return `invalid value for ${where}: ${String(v)}`;
7627
+ }
7628
+ }
7629
+ return null;
7630
+ }
7631
+ function validateHw(hw) {
7632
+ if (typeof hw !== "object" || hw === null || Array.isArray(hw)) return "hw must be an object";
7633
+ const h = hw;
7634
+ const { gpus, ...flat } = h;
7635
+ const flatError = checkFields(flat, HW_SPEC, "hw");
7636
+ if (flatError !== null) return flatError;
7637
+ if (!Array.isArray(gpus)) return "missing field: hw.gpus";
7638
+ for (const g of gpus) {
7639
+ const e = checkFields(g, { name: { kind: "ident" }, vramMb: { kind: "number" } }, "hw.gpus[]");
7640
+ if (e !== null) return e;
7641
+ }
7642
+ return null;
7643
+ }
7644
+ function validatePayload(eventName, payload) {
7645
+ if (eventName === "bench_result") {
7646
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
7647
+ return "bench_result requires an object payload";
7648
+ }
7649
+ const p = payload;
7650
+ for (const key of Object.keys(p)) {
7651
+ if (!Object.prototype.hasOwnProperty.call(BENCH_PAYLOAD, key)) return `unknown field: payload.${key}`;
7652
+ }
7653
+ for (const [block, spec2] of Object.entries(BENCH_PAYLOAD)) {
7654
+ const e = checkFields(p[block], spec2, `payload.${block}`);
7655
+ if (e !== null) return e;
7656
+ }
7657
+ return null;
7658
+ }
7659
+ const spec = PAYLOAD_SPECS[eventName];
7660
+ if (spec === void 0) {
7661
+ return payload === void 0 ? null : `${eventName} takes no payload`;
7662
+ }
7663
+ return checkFields(payload, spec, "payload");
7664
+ }
7665
+ function validateEvent(raw) {
7666
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
7667
+ return { ok: false, reason: "event must be an object" };
7668
+ }
7669
+ const e = raw;
7670
+ if (typeof e.event !== "string" || !EVENT_NAMES.includes(e.event)) {
7671
+ return { ok: false, reason: `unknown event name: ${String(e.event)}` };
7672
+ }
7673
+ for (const key of Object.keys(e)) {
7674
+ if (!ENVELOPE_KEYS.includes(key)) {
7675
+ return { ok: false, reason: `unknown field: ${key}` };
7676
+ }
7677
+ }
7678
+ if (e.schema !== TELEMETRY_SCHEMA_VERSION) {
7679
+ return { ok: false, reason: `invalid value for schema: ${String(e.schema)}` };
7680
+ }
7681
+ if (e.event === "consent_choice") {
7682
+ for (const banned of ["machineId", "app", "hw", "ts", "payload"]) {
7683
+ if (banned in e) return { ok: false, reason: `consent_choice must not carry ${banned}` };
7684
+ }
7685
+ if (typeof e.level !== "string" || !CONSENT_LEVELS.includes(e.level)) {
7686
+ return { ok: false, reason: `invalid value for level: ${String(e.level)}` };
7687
+ }
7688
+ return { ok: true, event: e };
7689
+ }
7690
+ if ("level" in e) return { ok: false, reason: "level is only valid on consent_choice" };
7691
+ if (typeof e.ts !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(e.ts)) {
7692
+ return { ok: false, reason: "missing or malformed field: ts" };
7693
+ }
7694
+ if (typeof e.machineId !== "string" || !/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(e.machineId)) {
7695
+ return { ok: false, reason: "missing or malformed field: machineId" };
7696
+ }
7697
+ const appError = checkFields(e.app, { version: { kind: "ident" }, os: { kind: "os" } }, "app");
7698
+ if (appError !== null) return { ok: false, reason: appError };
7699
+ if (e.event === "bench_result") {
7700
+ const hwError = validateHw(e.hw);
7701
+ if (hwError !== null) return { ok: false, reason: hwError };
7702
+ } else if ("hw" in e) {
7703
+ return { ok: false, reason: "hw is only valid on bench_result" };
7704
+ }
7705
+ const payloadError = validatePayload(e.event, e.payload);
7706
+ if (payloadError !== null) return { ok: false, reason: payloadError };
7707
+ return { ok: true, event: e };
7708
+ }
7709
+
7710
+ // src/telemetry/queue.ts
7711
+ var MAX_QUEUED_EVENTS = 500;
7712
+ function queueDir(dataDir) {
7713
+ return join19(dataDir, "telemetry", "queue");
7714
+ }
7715
+ function enqueue(dataDir, event) {
7716
+ try {
7717
+ const result = validateEvent(event);
7718
+ if (!result.ok) return false;
7719
+ const dir = queueDir(dataDir);
7720
+ mkdirSync10(dir, { recursive: true });
7721
+ writeFileSync8(join19(dir, `${Date.now()}-${randomUUID4()}.json`), JSON.stringify(result.event));
7722
+ trim(dir);
7723
+ return true;
7724
+ } catch {
7725
+ return false;
7726
+ }
7727
+ }
7728
+ function readQueue(dataDir) {
7729
+ const dir = queueDir(dataDir);
7730
+ let files;
7731
+ try {
7732
+ files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort();
7733
+ } catch {
7734
+ return [];
7735
+ }
7736
+ const out = [];
7737
+ for (const file of files) {
7738
+ try {
7739
+ const event = JSON.parse(readFileSync7(join19(dir, file), "utf8"));
7740
+ if (validateEvent(event).ok) out.push({ file, event });
7741
+ else remove(dataDir, file);
7742
+ } catch {
7743
+ remove(dataDir, file);
7744
+ }
7745
+ }
7746
+ return out;
7747
+ }
7748
+ function remove(dataDir, file) {
7749
+ try {
7750
+ rmSync10(join19(queueDir(dataDir), file), { force: true });
7751
+ } catch {
7752
+ }
7753
+ }
7754
+ function trim(dir) {
7755
+ try {
7756
+ const files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort();
7757
+ for (const stale of files.slice(0, Math.max(0, files.length - MAX_QUEUED_EVENTS))) {
7758
+ rmSync10(join19(dir, stale), { force: true });
7759
+ }
7760
+ } catch {
7761
+ }
7762
+ }
7763
+
7764
+ // src/telemetry/ledger.ts
7765
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, mkdirSync as mkdirSync11 } from "fs";
7766
+ import { join as join20 } from "path";
7767
+ var DAILY_KEEP = 30;
7768
+ function ledgerPath(dataDir) {
7769
+ return join20(dataDir, "telemetry", "ledger.json");
7770
+ }
7771
+ function read(dataDir) {
7772
+ try {
7773
+ const parsed = JSON.parse(readFileSync8(ledgerPath(dataDir), "utf8"));
7774
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
7775
+ } catch {
7776
+ return [];
7777
+ }
7778
+ }
7779
+ function claimOnce(dataDir, key) {
7780
+ try {
7781
+ const keys = read(dataDir);
7782
+ if (keys.includes(key)) return false;
7783
+ keys.push(key);
7784
+ const dailies = keys.filter((k) => k.startsWith("daily:")).slice(-DAILY_KEEP);
7785
+ const rest = keys.filter((k) => !k.startsWith("daily:"));
7786
+ mkdirSync11(join20(dataDir, "telemetry"), { recursive: true });
7787
+ writeFileSync9(ledgerPath(dataDir), JSON.stringify([...rest, ...dailies]));
7788
+ return true;
7789
+ } catch {
7790
+ return false;
7791
+ }
7792
+ }
7793
+
7794
+ // src/telemetry/first-load.ts
7795
+ function reportModelLoad(dataDir, emitter, outcome, failReason) {
7796
+ try {
7797
+ if (!emitter.canSend("model_first_load")) return;
7798
+ if (!claimOnce(dataDir, "once:model_first_load")) return;
7799
+ emitter.emit(
7800
+ "model_first_load",
7801
+ outcome === "fail" ? { outcome, failReason } : { outcome }
7802
+ );
7803
+ } catch {
7804
+ }
7805
+ }
7806
+
7807
+ // src/telemetry/classify.ts
7808
+ var OOM_SIGNS = [
7809
+ "out of memory",
7810
+ "outofmemory",
7811
+ "failed to allocate",
7812
+ "alloc_buffer",
7813
+ "memory allocation of size",
7814
+ "cudamalloc",
7815
+ "insufficient memory"
7816
+ ];
7817
+ var ARCH_SIGNS = ["unknown model architecture", "unsupported model architecture", "unsupported architecture"];
7818
+ var GGUF_SIGNS = ["invalid magic", "error loading model", "failed to load model", "gguf_init", "corrupt"];
7819
+ function classifyLoadFailure(err7) {
7820
+ if (!err7) return "other";
7821
+ if (err7.code === "readiness_timeout") return "timeout";
7822
+ if (err7.code === "engine_unsupported") return "no_engine";
7823
+ const haystack = [err7.message ?? "", ...err7.logTail ?? []].join("\n").toLowerCase();
7824
+ if (haystack.includes("no_active_engine")) return "no_engine";
7825
+ if (haystack.includes("no_such_model")) return "bad_gguf";
7826
+ if (OOM_SIGNS.some((s) => haystack.includes(s))) return "oom";
7827
+ if (ARCH_SIGNS.some((s) => haystack.includes(s))) return "unsupported_arch";
7828
+ if (GGUF_SIGNS.some((s) => haystack.includes(s))) return "bad_gguf";
7829
+ return "other";
7830
+ }
7831
+ function classifyBenchFailure(results) {
7832
+ if (results.some((r) => r.outcome === "oom")) return "oom";
7833
+ if (results.some((r) => r.outcome === "timeout")) return "timeout";
7834
+ return "other";
7835
+ }
7836
+
7837
+ // src/telemetry/disabled.ts
7838
+ var TELEMETRY_ENV = "TURBOLLM_TELEMETRY";
7839
+ var OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false", "no"]);
7840
+ function telemetryDisabled() {
7841
+ const raw = process.env[TELEMETRY_ENV];
7842
+ if (raw === void 0) return false;
7843
+ return OFF_VALUES.has(raw.trim().toLowerCase());
7844
+ }
7845
+
7446
7846
  // src/models/card-sampling.ts
7447
7847
  var FIELDS = [
7448
7848
  { key: "temp", alias: "temp(?:erature)?", min: 0, max: 2 },
@@ -7609,6 +8009,11 @@ var BenchRunner = class {
7609
8009
  logWinner = null;
7610
8010
  // The finished run's winning candidate, held (not persisted) until the user clicks Save.
7611
8011
  winning = null;
8012
+ /** Journey telemetry (ADR-299 `model_first_load`). Set post-construction in
8013
+ * cli.ts, matching `Manager.onLoadSettled`'s wiring — the Emitter is built
8014
+ * after BenchRunner in the daemon's startup order. Absent under tests, which
8015
+ * construct BenchRunner directly and never exercise `run()`. */
8016
+ telemetry;
7612
8017
  /** Live state for GET /status. */
7613
8018
  status() {
7614
8019
  return this.state;
@@ -7627,6 +8032,14 @@ var BenchRunner = class {
7627
8032
  this.cancelled = true;
7628
8033
  this.abort?.abort();
7629
8034
  }
8035
+ /** Report `model_first_load` for a sweep's conclusion (ADR-299). A no-op when
8036
+ * no Emitter was injected (absent under tests, which never call `run()`).
8037
+ * Only the FIRST sweep's outcome across an install's lifetime is recorded —
8038
+ * see `reportModelLoad`'s own once-only claim — so a re-tune later on
8039
+ * cannot overwrite it. */
8040
+ reportFirstLoad(outcome, failReason) {
8041
+ if (this.telemetry) reportModelLoad(this.store.dir(), this.telemetry, outcome, failReason);
8042
+ }
7630
8043
  /** Stop the engine — force-killed immediately when the run is cancelled (the user is actively
7631
8044
  * waiting for it to stop, ADR-220), gracefully otherwise (lets llama-server release resources
7632
8045
  * cleanly between candidates during a normal run). Every stop in this file goes through this
@@ -7739,11 +8152,13 @@ var BenchRunner = class {
7739
8152
  await this.stopEngine();
7740
8153
  }
7741
8154
  if (this.cancelled) {
8155
+ this.reportFirstLoad("cancelled");
7742
8156
  this.state = { running: false, modelKey, done: true, candidates: results };
7743
8157
  return;
7744
8158
  }
7745
8159
  const profile = recommended && hasAnySampling(recommended) ? { ...best.profile, sampling: { ...best.profile.sampling, ...recommended } } : best.profile;
7746
8160
  const kvAdvisory = kvSpeedAdvisory(best.cand.tps, profile.kvTypeK, caps.kvTypes);
8161
+ this.reportFirstLoad("ok");
7747
8162
  this.winning = { modelKey, profile, cand: best.cand, entry, sys, engineVersion: active?.version ?? "", engineId: active?.id ?? "" };
7748
8163
  this.logWinner = { params: best.cand.params, tps: best.cand.tps ?? 0, prefillTps: best.cand.prefillTps, ttftMs: best.cand.ttftMs ?? 0, vramMb: best.cand.vramMb };
7749
8164
  this.state = {
@@ -7773,6 +8188,7 @@ var BenchRunner = class {
7773
8188
  } else {
7774
8189
  const memoryBound = results.length > 0 && results.every((r) => r.outcome === "oom");
7775
8190
  const err7 = this.cancelled ? void 0 : memoryBound ? `This model doesn't fit on your GPU at ${baseProfile.ctx.toLocaleString()} context \u2014 even with maximum CPU offload it ran out of VRAM. Lower the context length and try again.` : "No candidate completed successfully.";
8191
+ this.reportFirstLoad(this.cancelled ? "cancelled" : "fail", this.cancelled ? void 0 : classifyBenchFailure(results));
7776
8192
  this.state = { running: false, modelKey, done: true, error: err7, candidates: results };
7777
8193
  }
7778
8194
  }
@@ -7982,16 +8398,16 @@ var BenchRunner = class {
7982
8398
  await this.stopEngine();
7983
8399
  return fail("crash");
7984
8400
  }
7985
- const logPath = this.manager.logPath();
8401
+ const logPath2 = this.manager.logPath();
7986
8402
  const benchMessages = buildBenchMessages(profile.ctx);
7987
8403
  phase("warming up\u2026");
7988
- const warm = await this.prefillProbe(target, benchMessages, remaining(), logPath, stepPrefix);
8404
+ const warm = await this.prefillProbe(target, benchMessages, remaining(), logPath2, stepPrefix);
7989
8405
  if (warm !== "ok") {
7990
8406
  await this.stopEngine();
7991
8407
  return fail(warm.fault);
7992
8408
  }
7993
8409
  phase("measuring t/s\u2026");
7994
- const measured = await this.runChatWatched(target, benchMessages, 128, remaining(), logPath);
8410
+ const measured = await this.runChatWatched(target, benchMessages, 128, remaining(), logPath2);
7995
8411
  const vramAfter = await readGpuVramMb(sys);
7996
8412
  await this.stopEngine();
7997
8413
  if ("fault" in measured) return fail(measured.fault);
@@ -8003,7 +8419,7 @@ var BenchRunner = class {
8003
8419
  * log (oom). Honors cancel + the global deadline. */
8004
8420
  async awaitReady(testDeadline) {
8005
8421
  const deadline = Math.min(Date.now() + READY_TIMEOUT_MS, testDeadline, this.deadline);
8006
- const logPath = this.manager.logPath();
8422
+ const logPath2 = this.manager.logPath();
8007
8423
  for (; ; ) {
8008
8424
  await sleep2(400);
8009
8425
  if (this.cancelled) return "crash";
@@ -8014,7 +8430,7 @@ var BenchRunner = class {
8014
8430
  if (tail.some((l) => OOM_RE.test(l))) return "oom";
8015
8431
  return "crash";
8016
8432
  }
8017
- if (logPath && OOM_RE.test(readLiveTail(logPath))) return "oom";
8433
+ if (logPath2 && OOM_RE.test(readLiveTail(logPath2))) return "oom";
8018
8434
  if (Date.now() > deadline) return "timeout";
8019
8435
  }
8020
8436
  }
@@ -8038,7 +8454,7 @@ var BenchRunner = class {
8038
8454
  * in seconds instead of hanging out the per-test budget. A watchdog polls the engine state + the
8039
8455
  * live engine log; on an OOM / "device not ready" / process death it aborts the request and the
8040
8456
  * result is classified accordingly. Returns the timing, or a `fault` outcome. */
8041
- async runChatWatched(target, messages, maxTokens, budgetMs, logPath) {
8457
+ async runChatWatched(target, messages, maxTokens, budgetMs, logPath2) {
8042
8458
  const probe2 = new AbortController();
8043
8459
  let fault = null;
8044
8460
  const watch = (async () => {
@@ -8055,7 +8471,7 @@ var BenchRunner = class {
8055
8471
  probe2.abort();
8056
8472
  return;
8057
8473
  }
8058
- if (logPath && OOM_RE.test(readLiveTail(logPath))) {
8474
+ if (logPath2 && OOM_RE.test(readLiveTail(logPath2))) {
8059
8475
  fault = "oom";
8060
8476
  probe2.abort();
8061
8477
  return;
@@ -8082,7 +8498,7 @@ var BenchRunner = class {
8082
8498
  * out the whole budget. Also aborts on an engine fault (OOM / "device not ready" / process death).
8083
8499
  * Returns 'ok' once prefill completes (generation starts) — a config that gets here is viable and
8084
8500
  * the warm prompt cache makes the following measured request fast and accurate. */
8085
- async prefillProbe(target, messages, budgetMs, logPath, stepPrefix) {
8501
+ async prefillProbe(target, messages, budgetMs, logPath2, stepPrefix) {
8086
8502
  const probe2 = new AbortController();
8087
8503
  let fault = null;
8088
8504
  const watch = (async () => {
@@ -8099,7 +8515,7 @@ var BenchRunner = class {
8099
8515
  probe2.abort();
8100
8516
  return;
8101
8517
  }
8102
- if (logPath && OOM_RE.test(readLiveTail(logPath))) {
8518
+ if (logPath2 && OOM_RE.test(readLiveTail(logPath2))) {
8103
8519
  fault = "oom";
8104
8520
  probe2.abort();
8105
8521
  return;
@@ -8321,21 +8737,23 @@ var BenchRunner = class {
8321
8737
  }
8322
8738
  /** Queue an anonymized bench_result telemetry event (spec 09 §3) — ONLY when
8323
8739
  * telemetry is on. Built from whitelisted fields only (never prompts, paths,
8324
- * tokens). No uploader (post-launch); just a queue file. Fully fail-safe. */
8740
+ * tokens). Validation, bounding and fail-safety live in `telemetry/queue`
8741
+ * (ADR-299); this method owns only the consent check and the payload shape. */
8325
8742
  queueTelemetry(record, entry, sys, appVersion, engineVersion) {
8326
8743
  try {
8744
+ if (telemetryDisabled()) return;
8327
8745
  const cfg2 = this.store.snapshot();
8328
8746
  const level = cfg2.telemetry.level;
8329
8747
  if (level !== "anon" && level !== "full") return;
8330
8748
  let machineId = cfg2.telemetry.machineId;
8331
8749
  if (!machineId) {
8332
- machineId = randomUUID4();
8750
+ machineId = randomUUID5();
8333
8751
  this.store.update((c) => {
8334
8752
  if (!c.telemetry.machineId) c.telemetry.machineId = machineId;
8335
8753
  });
8336
8754
  }
8337
- const event = {
8338
- schema: 1,
8755
+ enqueue(this.store.dir(), {
8756
+ schema: TELEMETRY_SCHEMA_VERSION,
8339
8757
  event: "bench_result",
8340
8758
  ts: record.ts,
8341
8759
  machineId,
@@ -8351,10 +8769,7 @@ var BenchRunner = class {
8351
8769
  params: record.params,
8352
8770
  result: { tps: record.tps, ttftMs: record.ttftMs, vramMb: record.vramMb, outcome: "ok" }
8353
8771
  }
8354
- };
8355
- const queueDir = join19(this.store.dir(), "telemetry", "queue");
8356
- mkdirSync10(queueDir, { recursive: true });
8357
- writeFileSync8(join19(queueDir, `${randomUUID4()}.json`), JSON.stringify(event));
8772
+ });
8358
8773
  } catch {
8359
8774
  }
8360
8775
  }
@@ -8589,7 +9004,7 @@ function sleep2(ms) {
8589
9004
  }
8590
9005
  function readLiveTail(path) {
8591
9006
  try {
8592
- return readFileSync7(path, "utf8").slice(-8e3);
9007
+ return readFileSync9(path, "utf8").slice(-8e3);
8593
9008
  } catch {
8594
9009
  return "";
8595
9010
  }
@@ -9678,11 +10093,11 @@ async function backfillPublishedDates(results) {
9678
10093
  try {
9679
10094
  const reader = resp.body.getReader();
9680
10095
  const decoder = new TextDecoder();
9681
- let read = 0;
9682
- while (read < DATE_PROBE_MAX_BYTES) {
10096
+ let read2 = 0;
10097
+ while (read2 < DATE_PROBE_MAX_BYTES) {
9683
10098
  const { done, value } = await reader.read();
9684
10099
  if (done) break;
9685
- read += value.byteLength;
10100
+ read2 += value.byteLength;
9686
10101
  html += decoder.decode(value, { stream: true });
9687
10102
  }
9688
10103
  await reader.cancel().catch(() => {
@@ -10297,7 +10712,7 @@ var GenerationGate = class {
10297
10712
  };
10298
10713
 
10299
10714
  // src/agents/task-state.ts
10300
- import { randomUUID as randomUUID5 } from "crypto";
10715
+ import { randomUUID as randomUUID6 } from "crypto";
10301
10716
  var MAX_TASKS = 50;
10302
10717
  var KEEP_DONE_MS = 5 * 6e4;
10303
10718
  var AgentTaskState = class {
@@ -10305,7 +10720,7 @@ var AgentTaskState = class {
10305
10720
  /** Begin a task; returns its id (use it for step/finish). */
10306
10721
  start(kind, agentId, label, convId) {
10307
10722
  this.gc();
10308
- const id = randomUUID5();
10723
+ const id = randomUUID6();
10309
10724
  this.tasks.set(id, { id, kind, convId, agentId, label, status: "running", steps: [], startedAt: Date.now() });
10310
10725
  return id;
10311
10726
  }
@@ -10356,7 +10771,7 @@ var AgentTaskState = class {
10356
10771
  import { spawn as spawn4 } from "child_process";
10357
10772
  import { homedir as homedir3 } from "os";
10358
10773
  import { readFile, writeFile, mkdir } from "fs/promises";
10359
- import { join as join20, dirname as dirname8 } from "path";
10774
+ import { join as join21, dirname as dirname8 } from "path";
10360
10775
  var AUTH_TOKEN = "turbollm-local";
10361
10776
  var SUPPORTED = {
10362
10777
  claude: {
@@ -10494,13 +10909,13 @@ Add the "turbollm" provider by hand (see the Connect screen: TurboLLM UI \u2192
10494
10909
  };
10495
10910
  }
10496
10911
  async function prepareOpencode(base2, apiKey, _modelKey, modelName, fs2 = realFs) {
10497
- const path = join20(fs2.home, ".config", "opencode", "opencode.json");
10498
- const read = await readConfigObject(fs2, path);
10499
- if ("corrupt" in read) return corruptConfigError(path, "opencode");
10500
- const cfg2 = read.obj;
10912
+ const path = join21(fs2.home, ".config", "opencode", "opencode.json");
10913
+ const read2 = await readConfigObject(fs2, path);
10914
+ if ("corrupt" in read2) return corruptConfigError(path, "opencode");
10915
+ const cfg2 = read2.obj;
10501
10916
  const provider = asObject(cfg2.provider);
10502
10917
  if (!provider) return corruptConfigError(path, "opencode");
10503
- if (read.lenient) {
10918
+ if (read2.lenient) {
10504
10919
  return providerAlreadyPointsHere(provider.turbollm, base2) ? { ok: true } : commentedConfigError(path, "opencode");
10505
10920
  }
10506
10921
  provider.turbollm = {
@@ -10514,13 +10929,13 @@ async function prepareOpencode(base2, apiKey, _modelKey, modelName, fs2 = realFs
10514
10929
  return { ok: true };
10515
10930
  }
10516
10931
  async function prepareKilo(base2, apiKey, _modelKey, modelName, fs2 = realFs) {
10517
- const path = join20(fs2.home, ".config", "kilo", "kilo.jsonc");
10518
- const read = await readConfigObject(fs2, path);
10519
- if ("corrupt" in read) return corruptConfigError(path, "Kilo Code");
10520
- const cfg2 = read.obj;
10932
+ const path = join21(fs2.home, ".config", "kilo", "kilo.jsonc");
10933
+ const read2 = await readConfigObject(fs2, path);
10934
+ if ("corrupt" in read2) return corruptConfigError(path, "Kilo Code");
10935
+ const cfg2 = read2.obj;
10521
10936
  const provider = asObject(cfg2.provider);
10522
10937
  if (!provider) return corruptConfigError(path, "Kilo Code");
10523
- if (read.lenient) {
10938
+ if (read2.lenient) {
10524
10939
  return providerAlreadyPointsHere(provider.turbollm, base2) ? { ok: true } : commentedConfigError(path, "Kilo Code");
10525
10940
  }
10526
10941
  provider.turbollm = {
@@ -10535,16 +10950,16 @@ async function prepareKilo(base2, apiKey, _modelKey, modelName, fs2 = realFs) {
10535
10950
  return { ok: true };
10536
10951
  }
10537
10952
  async function prepareOpenclaw(base2, apiKey, modelKey, modelName, fs2 = realFs) {
10538
- const path = join20(fs2.home, ".config", "openclaw", "openclaw.json");
10539
- const read = await readConfigObject(fs2, path);
10540
- if ("corrupt" in read) return corruptConfigError(path, "openclaw");
10541
- const cfg2 = read.obj;
10953
+ const path = join21(fs2.home, ".config", "openclaw", "openclaw.json");
10954
+ const read2 = await readConfigObject(fs2, path);
10955
+ if ("corrupt" in read2) return corruptConfigError(path, "openclaw");
10956
+ const cfg2 = read2.obj;
10542
10957
  const models = asObject(cfg2.models);
10543
10958
  const providers = models && asObject(models.providers);
10544
10959
  const agents = asObject(cfg2.agents);
10545
10960
  const defaults = agents && asObject(agents.defaults);
10546
10961
  if (!models || !providers || !agents || !defaults) return corruptConfigError(path, "openclaw");
10547
- if (read.lenient) {
10962
+ if (read2.lenient) {
10548
10963
  return providerAlreadyPointsHere(providers.turbollm, base2) ? { ok: true } : commentedConfigError(path, "openclaw");
10549
10964
  }
10550
10965
  providers.turbollm = {
@@ -10563,7 +10978,7 @@ async function prepareOpenclaw(base2, apiKey, modelKey, modelName, fs2 = realFs)
10563
10978
  return { ok: true };
10564
10979
  }
10565
10980
  async function preparePi(base2, apiKey, modelKey, modelName, fs2 = realFs) {
10566
- const modelsPath = join20(fs2.home, ".pi", "agent", "models.json");
10981
+ const modelsPath = join21(fs2.home, ".pi", "agent", "models.json");
10567
10982
  const modelsRead = await readConfigObject(fs2, modelsPath);
10568
10983
  if ("corrupt" in modelsRead) return corruptConfigError(modelsPath, "pi");
10569
10984
  const modelsCfg = modelsRead.obj;
@@ -10582,7 +10997,7 @@ async function preparePi(base2, apiKey, modelKey, modelName, fs2 = realFs) {
10582
10997
  await fs2.mkdir(dirname8(modelsPath));
10583
10998
  await fs2.writeFile(modelsPath, JSON.stringify(modelsCfg, null, 2) + "\n");
10584
10999
  }
10585
- const settingsPath = join20(fs2.home, ".pi", "agent", "settings.json");
11000
+ const settingsPath = join21(fs2.home, ".pi", "agent", "settings.json");
10586
11001
  const settingsRead = await readConfigObject(fs2, settingsPath);
10587
11002
  if ("corrupt" in settingsRead) return corruptConfigError(settingsPath, "pi");
10588
11003
  const settingsCfg = settingsRead.obj;
@@ -10818,18 +11233,18 @@ Install it: ${spec.install}
10818
11233
  }
10819
11234
 
10820
11235
  // src/daemon-pid.ts
10821
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, unlinkSync, existsSync as existsSync18 } from "fs";
10822
- import { join as join21 } from "path";
11236
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync10, unlinkSync, existsSync as existsSync18 } from "fs";
11237
+ import { join as join22 } from "path";
10823
11238
  import { execFileSync as execFileSync3 } from "child_process";
10824
11239
  function writePidfile(dir, pid, port2) {
10825
- const path = join21(dir, "daemon.pid");
10826
- writeFileSync9(path, JSON.stringify({ pid, port: port2 }));
11240
+ const path = join22(dir, "daemon.pid");
11241
+ writeFileSync10(path, JSON.stringify({ pid, port: port2 }));
10827
11242
  }
10828
11243
  function readPidfile(dir) {
10829
- const path = join21(dir, "daemon.pid");
11244
+ const path = join22(dir, "daemon.pid");
10830
11245
  if (!existsSync18(path)) return null;
10831
11246
  try {
10832
- const raw = readFileSync8(path, "utf8");
11247
+ const raw = readFileSync10(path, "utf8");
10833
11248
  const parsed = JSON.parse(raw);
10834
11249
  if (parsed !== null && typeof parsed === "object" && typeof parsed.pid === "number" && typeof parsed.port === "number") {
10835
11250
  return parsed;
@@ -10847,7 +11262,7 @@ function resolveDaemonPort(dir, explicitPort, fallbackPort) {
10847
11262
  }
10848
11263
  function removePidfile(dir) {
10849
11264
  try {
10850
- unlinkSync(join21(dir, "daemon.pid"));
11265
+ unlinkSync(join22(dir, "daemon.pid"));
10851
11266
  } catch {
10852
11267
  }
10853
11268
  }
@@ -11074,16 +11489,16 @@ function runMcpServer(baseUrl, version2) {
11074
11489
  // src/server.ts
11075
11490
  import { Hono } from "hono";
11076
11491
  import { cors } from "hono/cors";
11077
- import { existsSync as existsSync24, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
11492
+ import { existsSync as existsSync24, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
11078
11493
  import { fileURLToPath } from "url";
11079
- import { dirname as dirname11, join as join30, normalize as normalize2 } from "path";
11494
+ import { dirname as dirname11, join as join32, normalize as normalize2 } from "path";
11080
11495
  import { Agent, setGlobalDispatcher } from "undici";
11081
11496
 
11082
11497
  // src/api/routes.ts
11083
11498
  import { streamSSE as streamSSE2 } from "hono/streaming";
11084
- import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync11, readdirSync as readdirSync9, realpathSync, rmSync as rmSync13, statSync as statSync5, writeFileSync as writeFileSync12 } from "fs";
11499
+ import { existsSync as existsSync22, mkdirSync as mkdirSync16, readFileSync as readFileSync14, readdirSync as readdirSync10, realpathSync, rmSync as rmSync14, statSync as statSync5, writeFileSync as writeFileSync14 } from "fs";
11085
11500
  import { execFileSync as execFileSync6 } from "child_process";
11086
- import { basename as basename3, dirname as dirname9, join as join26, resolve } from "path";
11501
+ import { basename as basename3, dirname as dirname9, join as join28, resolve } from "path";
11087
11502
 
11088
11503
  // src/comfyui/gate-template.ts
11089
11504
  var GATE_VERSION = 2;
@@ -11222,7 +11637,7 @@ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
11222
11637
  }
11223
11638
 
11224
11639
  // src/api/routes.ts
11225
- import { randomUUID as randomUUID7 } from "crypto";
11640
+ import { randomUUID as randomUUID8 } from "crypto";
11226
11641
  import { homedir as homedir4, networkInterfaces as networkInterfaces2 } from "os";
11227
11642
 
11228
11643
  // src/chat/chat-routes.ts
@@ -11618,8 +12033,8 @@ function buildSaveSkillTool(onSaveSkill) {
11618
12033
  }
11619
12034
 
11620
12035
  // src/agents/skills.ts
11621
- import { join as join22 } from "path";
11622
- import { existsSync as existsSync19, readFileSync as readFileSync9, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, rmSync as rmSync10, readdirSync as readdirSync6 } from "fs";
12036
+ import { join as join23 } from "path";
12037
+ import { existsSync as existsSync19, readFileSync as readFileSync11, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, rmSync as rmSync11, readdirSync as readdirSync7 } from "fs";
11623
12038
  var BUILTIN_SKILLS = [
11624
12039
  {
11625
12040
  id: "filesystem",
@@ -11708,22 +12123,22 @@ var SkillStore = class {
11708
12123
  }
11709
12124
  dataDir;
11710
12125
  get skillsDir() {
11711
- return join22(this.dataDir, "skills");
12126
+ return join23(this.dataDir, "skills");
11712
12127
  }
11713
12128
  list() {
11714
12129
  const skills = [...BUILTIN_SKILLS];
11715
12130
  const idSet = new Set(skills.map((s) => s.id));
11716
12131
  if (!existsSync19(this.skillsDir)) return skills;
11717
- for (const e of readdirSync6(this.skillsDir, { withFileTypes: true })) {
12132
+ for (const e of readdirSync7(this.skillsDir, { withFileTypes: true })) {
11718
12133
  let skill = null;
11719
12134
  let id = "";
11720
12135
  if (e.isDirectory()) {
11721
12136
  id = e.name;
11722
- const md = join22(this.skillsDir, e.name, "SKILL.md");
12137
+ const md = join23(this.skillsDir, e.name, "SKILL.md");
11723
12138
  if (existsSync19(md)) skill = this.loadMd(id, md);
11724
12139
  } else if (e.isFile() && e.name.endsWith(".json")) {
11725
12140
  id = e.name.slice(0, -5);
11726
- skill = this.loadJson(join22(this.skillsDir, e.name), id);
12141
+ skill = this.loadJson(join23(this.skillsDir, e.name), id);
11727
12142
  }
11728
12143
  if (!skill || !id || idSet.has(id)) continue;
11729
12144
  idSet.add(id);
@@ -11739,30 +12154,30 @@ var SkillStore = class {
11739
12154
  return this.list().find((s) => s.id === id);
11740
12155
  }
11741
12156
  has(id) {
11742
- return isValidSkillId(id) && existsSync19(join22(this.skillsDir, id, "SKILL.md"));
12157
+ return isValidSkillId(id) && existsSync19(join23(this.skillsDir, id, "SKILL.md"));
11743
12158
  }
11744
12159
  write(skill) {
11745
12160
  if (!isValidSkillId(skill.id)) throw new Error(`Invalid skill id: ${skill.id}`);
11746
- const dir = join22(this.skillsDir, skill.id);
11747
- mkdirSync11(dir, { recursive: true });
11748
- writeFileSync10(join22(dir, "SKILL.md"), toSkillMd(skill), "utf8");
12161
+ const dir = join23(this.skillsDir, skill.id);
12162
+ mkdirSync12(dir, { recursive: true });
12163
+ writeFileSync11(join23(dir, "SKILL.md"), toSkillMd(skill), "utf8");
11749
12164
  }
11750
12165
  delete(id) {
11751
12166
  if (!isValidSkillId(id)) return;
11752
- rmSync10(join22(this.skillsDir, id), { recursive: true, force: true });
11753
- const legacy = join22(this.skillsDir, `${id}.json`);
11754
- if (existsSync19(legacy)) rmSync10(legacy);
12167
+ rmSync11(join23(this.skillsDir, id), { recursive: true, force: true });
12168
+ const legacy = join23(this.skillsDir, `${id}.json`);
12169
+ if (existsSync19(legacy)) rmSync11(legacy);
11755
12170
  }
11756
12171
  loadMd(id, path) {
11757
12172
  try {
11758
- return fromSkillMd(id, readFileSync9(path, "utf8"));
12173
+ return fromSkillMd(id, readFileSync11(path, "utf8"));
11759
12174
  } catch {
11760
12175
  return null;
11761
12176
  }
11762
12177
  }
11763
12178
  loadJson(path, id) {
11764
12179
  try {
11765
- const data = JSON.parse(readFileSync9(path, "utf8"));
12180
+ const data = JSON.parse(readFileSync11(path, "utf8"));
11766
12181
  if (!data.name || typeof data.name !== "string") return null;
11767
12182
  if (!data.instructions || typeof data.instructions !== "string") return null;
11768
12183
  return { id, name: data.name, description: data.description || "", instructions: data.instructions, tools: Array.isArray(data.tools) ? data.tools : [], builtin: false };
@@ -11784,7 +12199,7 @@ function importSkillsFromFolder(store2, folder, opts = {}) {
11784
12199
  if (depth > 5 || found.length >= max) return;
11785
12200
  let entries;
11786
12201
  try {
11787
- entries = readdirSync6(dir, { withFileTypes: true });
12202
+ entries = readdirSync7(dir, { withFileTypes: true });
11788
12203
  } catch {
11789
12204
  return;
11790
12205
  }
@@ -11792,14 +12207,14 @@ function importSkillsFromFolder(store2, folder, opts = {}) {
11792
12207
  if (skillFile) {
11793
12208
  const id = toSkillId(dir.split(/[\\/]/).pop() || "skill");
11794
12209
  try {
11795
- found.push({ id, text: readFileSync9(join22(dir, skillFile.name), "utf8") });
12210
+ found.push({ id, text: readFileSync11(join23(dir, skillFile.name), "utf8") });
11796
12211
  } catch {
11797
12212
  }
11798
12213
  return;
11799
12214
  }
11800
12215
  for (const e of entries) {
11801
12216
  if (found.length >= max) return;
11802
- if (e.isDirectory() && !e.name.startsWith(".")) walk2(join22(dir, e.name), depth + 1);
12217
+ if (e.isDirectory() && !e.name.startsWith(".")) walk2(join23(dir, e.name), depth + 1);
11803
12218
  }
11804
12219
  };
11805
12220
  walk2(folder, 0);
@@ -12960,7 +13375,7 @@ async function autoTitleFromConversation(d, convId) {
12960
13375
  }
12961
13376
 
12962
13377
  // src/auth.ts
12963
- import { createHash as createHash5, randomBytes, randomUUID as randomUUID6 } from "crypto";
13378
+ import { createHash as createHash5, randomBytes, randomUUID as randomUUID7 } from "crypto";
12964
13379
  import { getConnInfo } from "@hono/node-server/conninfo";
12965
13380
  var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
12966
13381
  function hashKey(raw) {
@@ -12978,7 +13393,7 @@ function generateApiKey() {
12978
13393
  function provisionTunnelApiKey(d) {
12979
13394
  const { full, hash, prefix } = generateApiKey();
12980
13395
  const key = {
12981
- id: randomUUID6(),
13396
+ id: randomUUID7(),
12982
13397
  name: `tunnel-${(/* @__PURE__ */ new Date()).toISOString()}`,
12983
13398
  hash,
12984
13399
  prefix,
@@ -13214,8 +13629,8 @@ async function claudePermissionModeChoices(run = realHelpRunner) {
13214
13629
 
13215
13630
  // src/terminal/terminal-manager.ts
13216
13631
  import { spawnSync as spawnSync2 } from "child_process";
13217
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync7, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "fs";
13218
- import { join as join23 } from "path";
13632
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, readdirSync as readdirSync8, rmSync as rmSync12, writeFileSync as writeFileSync12 } from "fs";
13633
+ import { join as join24 } from "path";
13219
13634
 
13220
13635
  // src/terminal/pty-session.ts
13221
13636
  import { EventEmitter } from "events";
@@ -13337,21 +13752,21 @@ var PTYSession = class _PTYSession {
13337
13752
 
13338
13753
  // src/terminal/terminal-manager.ts
13339
13754
  function terminalPidDir(dataDir) {
13340
- return join23(dataDir, "run");
13755
+ return join24(dataDir, "run");
13341
13756
  }
13342
13757
  function terminalPidFile(dataDir, pid) {
13343
- return join23(terminalPidDir(dataDir), `terminal-${pid}.pid`);
13758
+ return join24(terminalPidDir(dataDir), `terminal-${pid}.pid`);
13344
13759
  }
13345
13760
  function writeTerminalPid(dataDir, pid) {
13346
13761
  try {
13347
- mkdirSync12(terminalPidDir(dataDir), { recursive: true });
13348
- writeFileSync11(terminalPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
13762
+ mkdirSync13(terminalPidDir(dataDir), { recursive: true });
13763
+ writeFileSync12(terminalPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
13349
13764
  } catch {
13350
13765
  }
13351
13766
  }
13352
13767
  function clearTerminalPid(dataDir, pid) {
13353
13768
  try {
13354
- rmSync11(terminalPidFile(dataDir, pid), { force: true });
13769
+ rmSync12(terminalPidFile(dataDir, pid), { force: true });
13355
13770
  } catch {
13356
13771
  }
13357
13772
  }
@@ -13368,20 +13783,20 @@ function readTerminalPidFiles(dataDir) {
13368
13783
  const dir = terminalPidDir(dataDir);
13369
13784
  let names;
13370
13785
  try {
13371
- names = readdirSync7(dir).filter((n) => /^terminal-\d+\.pid$/.test(n));
13786
+ names = readdirSync8(dir).filter((n) => /^terminal-\d+\.pid$/.test(n));
13372
13787
  } catch {
13373
13788
  return [];
13374
13789
  }
13375
13790
  const out = [];
13376
13791
  for (const name of names) {
13377
- const file = join23(dir, name);
13792
+ const file = join24(dir, name);
13378
13793
  try {
13379
- const { pid, owner } = JSON.parse(readFileSync10(file, "utf8"));
13794
+ const { pid, owner } = JSON.parse(readFileSync12(file, "utf8"));
13380
13795
  if (typeof pid === "number" && pid > 0) out.push({ pid, owner: typeof owner === "number" ? owner : 0, file });
13381
- else rmSync11(file, { force: true });
13796
+ else rmSync12(file, { force: true });
13382
13797
  } catch {
13383
13798
  try {
13384
- rmSync11(file, { force: true });
13799
+ rmSync12(file, { force: true });
13385
13800
  } catch {
13386
13801
  }
13387
13802
  }
@@ -13399,7 +13814,7 @@ function reapStaleTerminals(dataDir) {
13399
13814
  } catch {
13400
13815
  }
13401
13816
  try {
13402
- rmSync11(file, { force: true });
13817
+ rmSync12(file, { force: true });
13403
13818
  } catch {
13404
13819
  }
13405
13820
  }
@@ -13414,7 +13829,7 @@ function killTrackedTerminalsSync(dataDir) {
13414
13829
  } catch {
13415
13830
  }
13416
13831
  try {
13417
- rmSync11(file, { force: true });
13832
+ rmSync12(file, { force: true });
13418
13833
  } catch {
13419
13834
  }
13420
13835
  }
@@ -14241,9 +14656,9 @@ function catalogEngine(id) {
14241
14656
 
14242
14657
  // src/engines/cuda-provision.ts
14243
14658
  import { execFile as execFile11 } from "child_process";
14244
- import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync8, rmSync as rmSync12 } from "fs";
14659
+ import { cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync14, readdirSync as readdirSync9, rmSync as rmSync13 } from "fs";
14245
14660
  import { tmpdir } from "os";
14246
- import { join as join24 } from "path";
14661
+ import { join as join25 } from "path";
14247
14662
  import { promisify as promisify8 } from "util";
14248
14663
  var execFileP8 = promisify8(execFile11);
14249
14664
  var REDIST_BASE = "https://developer.download.nvidia.com/compute/cuda/redist/";
@@ -14298,9 +14713,9 @@ async function provisionCuda(dataDir, onProgress, signal) {
14298
14713
  phase: "resolving",
14299
14714
  message: `Using CUDA ${version2}${driverMax ? ` (driver supports up to ${driverMax})` : ""}.`
14300
14715
  });
14301
- const root = join24(dataDir, "cuda", version2);
14302
- const binDir = join24(root, "bin");
14303
- if (existsSync20(join24(binDir, "nvcc.exe"))) {
14716
+ const root = join25(dataDir, "cuda", version2);
14717
+ const binDir = join25(root, "bin");
14718
+ if (existsSync20(join25(binDir, "nvcc.exe"))) {
14304
14719
  onProgress({ phase: "assembling", message: `CUDA ${version2} already downloaded \u2014 reusing it.` });
14305
14720
  return { root, binDir, version: version2 };
14306
14721
  }
@@ -14315,38 +14730,38 @@ async function provisionCuda(dataDir, onProgress, signal) {
14315
14730
  const rel = relOf(id);
14316
14731
  if (rel) components.push({ id, url: REDIST_BASE + rel });
14317
14732
  }
14318
- const work = join24(tmpdir(), `tllm-cuda-${version2}`);
14319
- rmSync12(work, { recursive: true, force: true });
14320
- mkdirSync13(work, { recursive: true });
14733
+ const work = join25(tmpdir(), `tllm-cuda-${version2}`);
14734
+ rmSync13(work, { recursive: true, force: true });
14735
+ mkdirSync14(work, { recursive: true });
14321
14736
  const stageRoot = `${root}.partial`;
14322
- rmSync12(stageRoot, { recursive: true, force: true });
14323
- mkdirSync13(stageRoot, { recursive: true });
14737
+ rmSync13(stageRoot, { recursive: true, force: true });
14738
+ mkdirSync14(stageRoot, { recursive: true });
14324
14739
  try {
14325
14740
  for (let i = 0; i < components.length; i++) {
14326
14741
  const { id, url } = components[i];
14327
- const zip = join24(work, `${id}.zip`);
14742
+ const zip = join25(work, `${id}.zip`);
14328
14743
  onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: 0 });
14329
14744
  await downloadFile(url, zip, (p) => onProgress({ phase: "downloading", message: `Downloading ${id} (${i + 1}/${components.length})\u2026`, pct: p.pct }), signal);
14330
- const exDir = join24(work, id);
14331
- mkdirSync13(exDir, { recursive: true });
14745
+ const exDir = join25(work, id);
14746
+ mkdirSync14(exDir, { recursive: true });
14332
14747
  onProgress({ phase: "extracting", message: `Extracting ${id}\u2026` });
14333
14748
  await extractArchive(zip, exDir);
14334
- const inner = readdirSync8(exDir).map((n) => join24(exDir, n)).find((p) => existsSync20(join24(p, "include")) || existsSync20(join24(p, "bin")) || existsSync20(join24(p, "lib")));
14335
- const srcRoot = inner ?? join24(exDir, readdirSync8(exDir)[0] ?? "");
14749
+ const inner = readdirSync9(exDir).map((n) => join25(exDir, n)).find((p) => existsSync20(join25(p, "include")) || existsSync20(join25(p, "bin")) || existsSync20(join25(p, "lib")));
14750
+ const srcRoot = inner ?? join25(exDir, readdirSync9(exDir)[0] ?? "");
14336
14751
  cpSync2(srcRoot, stageRoot, { recursive: true, force: true });
14337
- rmSync12(zip, { force: true });
14752
+ rmSync13(zip, { force: true });
14338
14753
  }
14339
- if (!existsSync20(join24(stageRoot, "bin", "nvcc.exe"))) {
14754
+ if (!existsSync20(join25(stageRoot, "bin", "nvcc.exe"))) {
14340
14755
  throw new Error("Assembled CUDA toolkit is missing bin/nvcc.exe \u2014 the component set may have changed.");
14341
14756
  }
14342
- rmSync12(root, { recursive: true, force: true });
14343
- mkdirSync13(join24(dataDir, "cuda"), { recursive: true });
14757
+ rmSync13(root, { recursive: true, force: true });
14758
+ mkdirSync14(join25(dataDir, "cuda"), { recursive: true });
14344
14759
  cpSync2(stageRoot, root, { recursive: true, force: true });
14345
14760
  onProgress({ phase: "assembling", message: `CUDA ${version2} ready.` });
14346
14761
  return { root, binDir, version: version2 };
14347
14762
  } finally {
14348
- rmSync12(work, { recursive: true, force: true });
14349
- rmSync12(stageRoot, { recursive: true, force: true });
14763
+ rmSync13(work, { recursive: true, force: true });
14764
+ rmSync13(stageRoot, { recursive: true, force: true });
14350
14765
  }
14351
14766
  }
14352
14767
 
@@ -14423,7 +14838,7 @@ function recommendEngines(p, engines) {
14423
14838
  // src/artifacts/screenshot.ts
14424
14839
  import { existsSync as existsSync21 } from "fs";
14425
14840
  import { execFileSync as execFileSync5 } from "child_process";
14426
- import { join as join25 } from "path";
14841
+ import { join as join26 } from "path";
14427
14842
  function chromeCandidates() {
14428
14843
  const env = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH;
14429
14844
  const out = env ? [env] : [];
@@ -14432,10 +14847,10 @@ function chromeCandidates() {
14432
14847
  const pfx86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
14433
14848
  const local = process.env["LOCALAPPDATA"] || "";
14434
14849
  for (const base2 of [pf, pfx86, local].filter(Boolean)) {
14435
- out.push(join25(base2, "Google", "Chrome", "Application", "chrome.exe"));
14850
+ out.push(join26(base2, "Google", "Chrome", "Application", "chrome.exe"));
14436
14851
  }
14437
14852
  for (const base2 of [pfx86, pf].filter(Boolean)) {
14438
- out.push(join25(base2, "Microsoft", "Edge", "Application", "msedge.exe"));
14853
+ out.push(join26(base2, "Microsoft", "Edge", "Application", "msedge.exe"));
14439
14854
  }
14440
14855
  } else if (process.platform === "darwin") {
14441
14856
  out.push(
@@ -14516,6 +14931,86 @@ async function screenshotArtifact(html, opts) {
14516
14931
  }
14517
14932
  }
14518
14933
 
14934
+ // src/telemetry/log.ts
14935
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync13, mkdirSync as mkdirSync15 } from "fs";
14936
+ import { join as join27 } from "path";
14937
+ var MAX_LOGGED = 200;
14938
+ function logPath(dataDir) {
14939
+ return join27(dataDir, "telemetry", "sent.json");
14940
+ }
14941
+ function readSentLog(dataDir) {
14942
+ try {
14943
+ const parsed = JSON.parse(readFileSync13(logPath(dataDir), "utf8"));
14944
+ return Array.isArray(parsed) ? parsed : [];
14945
+ } catch {
14946
+ return [];
14947
+ }
14948
+ }
14949
+ function recordSent(dataDir, events) {
14950
+ try {
14951
+ const sentAt = (/* @__PURE__ */ new Date()).toISOString();
14952
+ const fresh = events.map((event) => ({ sentAt, event })).reverse();
14953
+ const next = [...fresh, ...readSentLog(dataDir)].slice(0, MAX_LOGGED);
14954
+ mkdirSync15(join27(dataDir, "telemetry"), { recursive: true });
14955
+ writeFileSync13(logPath(dataDir), JSON.stringify(next));
14956
+ } catch {
14957
+ }
14958
+ }
14959
+
14960
+ // src/telemetry/uploader.ts
14961
+ var INGEST_URL = "https://t.turbollm.dev/v1/events";
14962
+ function mayTransmit(level) {
14963
+ return level === "anon" || level === "full";
14964
+ }
14965
+ async function flush(dataDir, level, transport = httpTransport) {
14966
+ try {
14967
+ const queued = readQueue(dataDir);
14968
+ if (telemetryDisabled() || !mayTransmit(level)) {
14969
+ for (const q of queued) remove(dataDir, q.file);
14970
+ return;
14971
+ }
14972
+ if (queued.length === 0) return;
14973
+ const events = queued.map((q) => q.event);
14974
+ const accepted = await transport(events);
14975
+ if (accepted) {
14976
+ recordSent(dataDir, events);
14977
+ for (const q of queued) remove(dataDir, q.file);
14978
+ }
14979
+ } catch {
14980
+ }
14981
+ }
14982
+ var httpTransport = async (events) => {
14983
+ const signal = AbortSignal.timeout(1e4);
14984
+ const res = await fetch(INGEST_URL, {
14985
+ method: "POST",
14986
+ headers: { "content-type": "application/json" },
14987
+ body: JSON.stringify(events),
14988
+ signal
14989
+ });
14990
+ return res.ok;
14991
+ };
14992
+
14993
+ // src/telemetry/consent.ts
14994
+ var httpTransport2 = async (events) => {
14995
+ const res = await fetch(INGEST_URL, {
14996
+ method: "POST",
14997
+ headers: { "content-type": "application/json" },
14998
+ body: JSON.stringify(events),
14999
+ signal: AbortSignal.timeout(1e4)
15000
+ });
15001
+ return res.ok;
15002
+ };
15003
+ async function sendConsentChoice(dataDir, level, transport = httpTransport2) {
15004
+ try {
15005
+ if (telemetryDisabled()) return;
15006
+ if (!CONSENT_LEVELS.includes(level)) return;
15007
+ if (!claimOnce(dataDir, "consent_choice_sent")) return;
15008
+ const ping = { schema: TELEMETRY_SCHEMA_VERSION, event: "consent_choice", level };
15009
+ if (await transport([ping])) recordSent(dataDir, [ping]);
15010
+ } catch {
15011
+ }
15012
+ }
15013
+
14519
15014
  // src/api/routes.ts
14520
15015
  function err3(c, status, code, message) {
14521
15016
  return c.json({ error: { code, message } }, status);
@@ -14577,7 +15072,7 @@ function registerApi(app2, d) {
14577
15072
  let installedVersion = null;
14578
15073
  if (gatePath) {
14579
15074
  try {
14580
- const src = readFileSync11(join26(gatePath, "__init__.py"), "utf-8");
15075
+ const src = readFileSync14(join28(gatePath, "__init__.py"), "utf-8");
14581
15076
  const m = src.match(/^GATE_VERSION\s*=\s*(\d+)/m);
14582
15077
  if (m) installedVersion = Number(m[1]);
14583
15078
  } catch {
@@ -14617,7 +15112,7 @@ function registerApi(app2, d) {
14617
15112
  const sys = getSysInfo();
14618
15113
  const vendor = primaryVendor(sys);
14619
15114
  const recommended = recommendBackendId(vendor, sys.gpus.length > 0);
14620
- const root = join26(d.store.dir(), "engines");
15115
+ const root = join28(d.store.dir(), "engines");
14621
15116
  const active = d.registry.active();
14622
15117
  const regEngines = d.registry.list().engines;
14623
15118
  const backendOf = (p) => p.match(/llama\.cpp-[^/\\]+-(cuda|rocm|sycl|vulkan|metal|cpu)/i)?.[1]?.toLowerCase();
@@ -14638,7 +15133,7 @@ function registerApi(app2, d) {
14638
15133
  engineId: eng?.id ?? ""
14639
15134
  };
14640
15135
  });
14641
- const mlxVenvPy = join26(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
15136
+ const mlxVenvPy = join28(root, "mlx", "venv", process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
14642
15137
  const mlxInstalledOnDisk = existsSync22(mlxVenvPy);
14643
15138
  const mlxEngine = regEngines.find((e) => e.kind === "mlx");
14644
15139
  const mlx = {
@@ -14654,7 +15149,7 @@ function registerApi(app2, d) {
14654
15149
  const b = await body3(c);
14655
15150
  const def = availableBackends().find((x) => x.id === b.backend);
14656
15151
  if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14657
- const root = join26(d.store.dir(), "engines");
15152
+ const root = join28(d.store.dir(), "engines");
14658
15153
  const existing = installedBackendBuild(root, def.id);
14659
15154
  if (existing) {
14660
15155
  return c.json({ accepted: false, alreadyInstalled: true, build: existing.tag });
@@ -14698,7 +15193,7 @@ function registerApi(app2, d) {
14698
15193
  app2.post("/api/v1/engines/backends/:id/update", async (c) => {
14699
15194
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
14700
15195
  if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14701
- const root = join26(d.store.dir(), "engines");
15196
+ const root = join28(d.store.dir(), "engines");
14702
15197
  const oldBin = installedBackendBuild(root, def.id)?.bin ?? null;
14703
15198
  if (!oldBin) return err3(c, 409, "not_installed", "Backend is not installed \u2014 download it first.");
14704
15199
  {
@@ -14742,7 +15237,7 @@ function registerApi(app2, d) {
14742
15237
  }
14743
15238
  }
14744
15239
  if (installedTag && installedTag !== latestTag) {
14745
- rmSync13(backendDir(root, def.id, installedTag), { recursive: true, force: true });
15240
+ rmSync14(backendDir(root, def.id, installedTag), { recursive: true, force: true });
14746
15241
  }
14747
15242
  d.provision.done();
14748
15243
  } catch (e) {
@@ -14757,7 +15252,7 @@ function registerApi(app2, d) {
14757
15252
  app2.post("/api/v1/engines/backends/:id/enable", async (c) => {
14758
15253
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
14759
15254
  if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14760
- const root = join26(d.store.dir(), "engines");
15255
+ const root = join28(d.store.dir(), "engines");
14761
15256
  const build2 = installedBackendBuild(root, def.id);
14762
15257
  if (!build2) return err3(c, 409, "not_installed", "Backend is not installed on disk \u2014 download it first.");
14763
15258
  let eng = d.registry.list().engines.find((e) => e.binPath === build2.bin);
@@ -14768,7 +15263,7 @@ function registerApi(app2, d) {
14768
15263
  app2.delete("/api/v1/engines/backends/:id", async (c) => {
14769
15264
  const def = availableBackends().find((x) => x.id === c.req.param("id"));
14770
15265
  if (!def) return err3(c, 400, "invalid_config_value", "Unknown backend for this platform.");
14771
- const root = join26(d.store.dir(), "engines");
15266
+ const root = join28(d.store.dir(), "engines");
14772
15267
  const backendDirRe = new RegExp(`[\\\\/]engines[\\\\/]llama\\.cpp-[^\\\\/]+-${def.id}[\\\\/]`);
14773
15268
  const engs = d.registry.list().engines.filter((e) => backendDirRe.test(e.binPath));
14774
15269
  const activeId = d.registry.active()?.id;
@@ -14790,7 +15285,7 @@ function registerApi(app2, d) {
14790
15285
  const busy = engineWorkBusy(d);
14791
15286
  if (busy) return err3(c, 409, "engine_already_running", busy);
14792
15287
  }
14793
- const root = join26(d.store.dir(), "engines");
15288
+ const root = join28(d.store.dir(), "engines");
14794
15289
  const upgrade = c.req.query("update") === "1";
14795
15290
  void (async () => {
14796
15291
  try {
@@ -14813,7 +15308,7 @@ function registerApi(app2, d) {
14813
15308
  const busy = engineWorkBusy(d);
14814
15309
  if (busy) return err3(c, 409, "engine_already_running", busy);
14815
15310
  }
14816
- const root = join26(d.store.dir(), "engines");
15311
+ const root = join28(d.store.dir(), "engines");
14817
15312
  const upgrade = c.req.query("update") === "1";
14818
15313
  void (async () => {
14819
15314
  try {
@@ -14830,12 +15325,12 @@ function registerApi(app2, d) {
14830
15325
  });
14831
15326
  app2.get("/api/v1/engines/catalog", (c) => {
14832
15327
  const regEngines = d.registry.list().engines;
14833
- const enginesRoot = join26(d.store.dir(), "engines");
15328
+ const enginesRoot = join28(d.store.dir(), "engines");
14834
15329
  const items = catalogForPlatform().map((e) => {
14835
15330
  let installed2;
14836
15331
  let enabled;
14837
15332
  if (e.provision === "pip") {
14838
- const pyPath = join26(
15333
+ const pyPath = join28(
14839
15334
  enginesRoot,
14840
15335
  e.id,
14841
15336
  "venv",
@@ -14844,7 +15339,7 @@ function registerApi(app2, d) {
14844
15339
  installed2 = existsSync22(pyPath);
14845
15340
  enabled = regEngines.some((x) => x.kind === e.kind);
14846
15341
  } else if (e.id === "turboquant") {
14847
- const tqDir = join26(enginesRoot, "turboquant");
15342
+ const tqDir = join28(enginesRoot, "turboquant");
14848
15343
  installed2 = existsSync22(tqDir);
14849
15344
  enabled = regEngines.some((x) => /[\\/]engines[\\/]turboquant[\\/]/.test(x.binPath));
14850
15345
  } else if (e.id === "koboldcpp") {
@@ -14914,9 +15409,9 @@ function registerApi(app2, d) {
14914
15409
  const busy = engineWorkBusy(d);
14915
15410
  if (busy) return err3(c, 409, "engine_already_running", busy);
14916
15411
  const name = (b.name ?? "").trim() || void 0;
14917
- const enginesRoot = join26(d.store.dir(), "engines");
15412
+ const enginesRoot = join28(d.store.dir(), "engines");
14918
15413
  const toolchainDirs = d.store.snapshot().build.toolchainDirs;
14919
- const buildRoot = join26(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
15414
+ const buildRoot = join28(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
14920
15415
  const ac = new AbortController();
14921
15416
  buildAbort = ac;
14922
15417
  d.build.start(name ?? repoUrl);
@@ -14931,7 +15426,7 @@ function registerApi(app2, d) {
14931
15426
  );
14932
15427
  } catch (e) {
14933
15428
  try {
14934
- rmSync13(buildRoot, { recursive: true, force: true });
15429
+ rmSync14(buildRoot, { recursive: true, force: true });
14935
15430
  } catch {
14936
15431
  }
14937
15432
  if (e?.name === "AbortError") {
@@ -15026,7 +15521,7 @@ function registerApi(app2, d) {
15026
15521
  const busy = engineWorkBusy(d);
15027
15522
  if (busy) return err3(c, 409, "engine_already_running", busy);
15028
15523
  }
15029
- const root = join26(d.store.dir(), "engines");
15524
+ const root = join28(d.store.dir(), "engines");
15030
15525
  const upgrade = c.req.query("update") === "1";
15031
15526
  void (async () => {
15032
15527
  try {
@@ -15046,7 +15541,7 @@ function registerApi(app2, d) {
15046
15541
  const busy = engineWorkBusy(d);
15047
15542
  if (busy) return err3(c, 409, "engine_already_running", busy);
15048
15543
  }
15049
- const root = join26(d.store.dir(), "engines");
15544
+ const root = join28(d.store.dir(), "engines");
15050
15545
  const upgrade = c.req.query("update") === "1";
15051
15546
  void (async () => {
15052
15547
  try {
@@ -15071,14 +15566,14 @@ function registerApi(app2, d) {
15071
15566
  const busy = engineWorkBusy(d);
15072
15567
  if (busy) return err3(c, 409, "engine_already_running", busy);
15073
15568
  }
15074
- const root = join26(d.store.dir(), "engines");
15569
+ const root = join28(d.store.dir(), "engines");
15075
15570
  const upgrade = c.req.query("update") === "1";
15076
15571
  void (async () => {
15077
15572
  try {
15078
15573
  d.provision.start("turboquant");
15079
15574
  if (upgrade) {
15080
- const tqDir = join26(root, "turboquant");
15081
- if (existsSync22(tqDir)) rmSync13(tqDir, { recursive: true, force: true });
15575
+ const tqDir = join28(root, "turboquant");
15576
+ if (existsSync22(tqDir)) rmSync14(tqDir, { recursive: true, force: true });
15082
15577
  }
15083
15578
  const bin = await provisionTurboquant(
15084
15579
  root,
@@ -15106,7 +15601,7 @@ function registerApi(app2, d) {
15106
15601
  const busy = engineWorkBusy(d);
15107
15602
  if (busy) return err3(c, 409, "engine_already_running", busy);
15108
15603
  }
15109
- const root = join26(d.store.dir(), "engines");
15604
+ const root = join28(d.store.dir(), "engines");
15110
15605
  const upgrade = c.req.query("update") === "1";
15111
15606
  const hasNvidia = primaryVendor(getSysInfo()) === "nvidia";
15112
15607
  void (async () => {
@@ -15114,7 +15609,7 @@ function registerApi(app2, d) {
15114
15609
  d.provision.start("koboldcpp");
15115
15610
  if (upgrade) {
15116
15611
  const dir = koboldcppDir(root);
15117
- if (existsSync22(dir)) rmSync13(dir, { recursive: true, force: true });
15612
+ if (existsSync22(dir)) rmSync14(dir, { recursive: true, force: true });
15118
15613
  }
15119
15614
  const rt = await ensureKoboldcpp(root, hasNvidia, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
15120
15615
  const eng = d.registry.addKoboldcpp(`KoboldCpp (${rt.version})`, rt.binPath, rt.version);
@@ -15137,14 +15632,14 @@ function registerApi(app2, d) {
15137
15632
  const busy = engineWorkBusy(d);
15138
15633
  if (busy) return err3(c, 409, "engine_already_running", busy);
15139
15634
  }
15140
- const root = join26(d.store.dir(), "engines");
15635
+ const root = join28(d.store.dir(), "engines");
15141
15636
  const upgrade = c.req.query("update") === "1";
15142
15637
  void (async () => {
15143
15638
  try {
15144
15639
  d.provision.start("llamafile");
15145
15640
  if (upgrade) {
15146
15641
  const dir = llamafileDir(root);
15147
- if (existsSync22(dir)) rmSync13(dir, { recursive: true, force: true });
15642
+ if (existsSync22(dir)) rmSync14(dir, { recursive: true, force: true });
15148
15643
  }
15149
15644
  const rt = await ensureLlamafile(root, (p) => d.provision.progress(p.phase, p.pct, p.part, p.parts));
15150
15645
  const eng = d.registry.addLlamafile(`llamafile (${rt.version})`, rt.binPath, rt.version);
@@ -15244,10 +15739,10 @@ function registerApi(app2, d) {
15244
15739
  }
15245
15740
  d.registry.remove(id);
15246
15741
  if (purge && eng) {
15247
- const enginesRoot = join26(d.store.dir(), "engines");
15742
+ const enginesRoot = join28(d.store.dir(), "engines");
15248
15743
  const purgeDir = engineInstallDir(eng, enginesRoot);
15249
15744
  if (purgeDir && existsSync22(purgeDir)) {
15250
- rmSync13(purgeDir, { recursive: true, force: true });
15745
+ rmSync14(purgeDir, { recursive: true, force: true });
15251
15746
  }
15252
15747
  d.registry.forgetCustomSource(customSourceKey(eng));
15253
15748
  }
@@ -15329,16 +15824,16 @@ function registerApi(app2, d) {
15329
15824
  }
15330
15825
  let entries;
15331
15826
  try {
15332
- entries = readdirSync9(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
15827
+ entries = readdirSync10(real, { withFileTypes: true }).filter((d2) => !d2.name.startsWith(".")).map((d2) => {
15333
15828
  let isDir = d2.isDirectory();
15334
15829
  if (d2.isSymbolicLink()) {
15335
15830
  try {
15336
- isDir = statSync5(join26(real, d2.name)).isDirectory();
15831
+ isDir = statSync5(join28(real, d2.name)).isDirectory();
15337
15832
  } catch {
15338
15833
  isDir = false;
15339
15834
  }
15340
15835
  }
15341
- return { name: d2.name, path: join26(real, d2.name), isDir };
15836
+ return { name: d2.name, path: join28(real, d2.name), isDir };
15342
15837
  }).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
15343
15838
  } catch {
15344
15839
  return err3(c, 400, "fs_read_failed", "Could not read that folder (permission denied or not a directory).");
@@ -15481,17 +15976,17 @@ function registerApi(app2, d) {
15481
15976
  }
15482
15977
  let customNodes;
15483
15978
  if (basename3(root).toLowerCase() === "custom_nodes") customNodes = root;
15484
- else if (existsSync22(join26(root, "custom_nodes"))) customNodes = join26(root, "custom_nodes");
15979
+ else if (existsSync22(join28(root, "custom_nodes"))) customNodes = join28(root, "custom_nodes");
15485
15980
  else {
15486
15981
  return err3(c, 400, "invalid_config_value", "No 'custom_nodes' folder here. Point me at your ComfyUI folder or its custom_nodes folder.");
15487
15982
  }
15488
15983
  const reqUrl = new URL(c.req.url);
15489
15984
  const port2 = reqUrl.port || (reqUrl.protocol === "https:" ? "443" : "80");
15490
15985
  const base2 = `http://127.0.0.1:${port2}`;
15491
- const gateDir = join26(customNodes, "turbollm_gate");
15986
+ const gateDir = join28(customNodes, "turbollm_gate");
15492
15987
  try {
15493
- mkdirSync14(gateDir, { recursive: true });
15494
- writeFileSync12(join26(gateDir, "__init__.py"), gateNodeSource(base2));
15988
+ mkdirSync16(gateDir, { recursive: true });
15989
+ writeFileSync14(join28(gateDir, "__init__.py"), gateNodeSource(base2));
15495
15990
  } catch (e) {
15496
15991
  return err3(c, 500, "fs_write_failed", `Could not write the gate node: ${e instanceof Error ? e.message : e}`);
15497
15992
  }
@@ -15504,7 +15999,7 @@ function registerApi(app2, d) {
15504
15999
  const dir = d.store.snapshot().comfyui.gatePath;
15505
16000
  if (dir && existsSync22(dir)) {
15506
16001
  try {
15507
- rmSync13(dir, { recursive: true, force: true });
16002
+ rmSync14(dir, { recursive: true, force: true });
15508
16003
  } catch (e) {
15509
16004
  return err3(c, 500, "fs_write_failed", `Could not remove the gate node: ${e instanceof Error ? e.message : e}`);
15510
16005
  }
@@ -15538,7 +16033,7 @@ function registerApi(app2, d) {
15538
16033
  while (!aborted) {
15539
16034
  const path = d.manager.logPath();
15540
16035
  if (path && existsSync22(path)) {
15541
- const lines = readFileSync11(path, "utf8").split("\n");
16036
+ const lines = readFileSync14(path, "utf8").split("\n");
15542
16037
  for (; sent < lines.length - 1; sent++) {
15543
16038
  await stream.writeSSE({ event: "line", data: JSON.stringify({ line: lines[sent].replace(/\r$/, "") }) });
15544
16039
  }
@@ -15829,6 +16324,7 @@ function registerApi(app2, d) {
15829
16324
  d.tools?.updateConfig(d.store.snapshot().tools);
15830
16325
  await d.tools?.buildToolDefinitions();
15831
16326
  }
16327
+ if (telemetryLevel !== void 0) void sendConsentChoice(d.store.dir(), telemetryLevel);
15832
16328
  const after = d.store.snapshot().daemon;
15833
16329
  const lanChanged = after.lanBind !== before.lanBind;
15834
16330
  const portChanged = after.port !== before.port;
@@ -15847,9 +16343,9 @@ function registerApi(app2, d) {
15847
16343
  if (transport === "stdio" && !b.command?.trim()) return err3(c, 400, "invalid_config_value", "command is required for stdio transport.");
15848
16344
  if (transport === "sse" && !b.url?.trim()) return err3(c, 400, "invalid_config_value", "url is required for sse transport.");
15849
16345
  if (transport === "sse" && !/^https?:\/\//i.test(b.url ?? "")) return err3(c, 400, "invalid_config_value", "url must be an http(s):// address.");
15850
- const { randomUUID: randomUUID11 } = await import("crypto");
16346
+ const { randomUUID: randomUUID13 } = await import("crypto");
15851
16347
  const server2 = {
15852
- id: randomUUID11(),
16348
+ id: randomUUID13(),
15853
16349
  name: b.name.trim(),
15854
16350
  transport,
15855
16351
  enabled: b.enabled !== false,
@@ -15857,7 +16353,7 @@ function registerApi(app2, d) {
15857
16353
  };
15858
16354
  const isMemoryServer = server2.transport === "stdio" && (server2.command?.includes("server-memory") || server2.args?.some((a) => a.includes("server-memory")));
15859
16355
  if (isMemoryServer && !server2.env?.MEMORY_FILE_PATH) {
15860
- server2.env = { ...server2.env, MEMORY_FILE_PATH: join26(homedir4(), ".turbollm", "mcp-memory.jsonl") };
16356
+ server2.env = { ...server2.env, MEMORY_FILE_PATH: join28(homedir4(), ".turbollm", "mcp-memory.jsonl") };
15861
16357
  }
15862
16358
  d.store.update((cfg2) => {
15863
16359
  cfg2.mcp.servers.push(server2);
@@ -15911,6 +16407,15 @@ function registerApi(app2, d) {
15911
16407
  const level = ["off", "anon", "full"].includes(raw) ? raw : "off";
15912
16408
  return c.json(telemetryPreview(level, d.version));
15913
16409
  });
16410
+ app2.get("/api/v1/telemetry/log", (c) => c.json({ entries: readSentLog(d.store.dir()) }));
16411
+ app2.post("/api/v1/telemetry/regenerate-id", (c) => {
16412
+ const machineId = randomUUID8();
16413
+ d.store.update((cfg2) => {
16414
+ cfg2.telemetry.machineId = machineId;
16415
+ });
16416
+ for (const q of readQueue(d.store.dir())) remove(d.store.dir(), q.file);
16417
+ return c.json({ machineId });
16418
+ });
15914
16419
  app2.get("/api/v1/settings/network", (c) => {
15915
16420
  const cfg2 = d.store.snapshot();
15916
16421
  return c.json({
@@ -16062,7 +16567,7 @@ function registerApi(app2, d) {
16062
16567
  if (!name) return err3(c, 400, "invalid_config_value", "name is required.");
16063
16568
  const { full, hash, prefix } = generateApiKey();
16064
16569
  const key = {
16065
- id: randomUUID7(),
16570
+ id: randomUUID8(),
16066
16571
  name,
16067
16572
  hash,
16068
16573
  prefix,
@@ -16098,7 +16603,7 @@ function registerApi(app2, d) {
16098
16603
  const keyName = `cli-${cli}`;
16099
16604
  const { full, hash, prefix } = generateApiKey();
16100
16605
  const fresh = {
16101
- id: randomUUID7(),
16606
+ id: randomUUID8(),
16102
16607
  name: keyName,
16103
16608
  hash,
16104
16609
  prefix,
@@ -16162,6 +16667,13 @@ function settingsPayload(d) {
16162
16667
  lanBind: cfg2.daemon.lanBind,
16163
16668
  requireApiKey: cfg2.daemon.requireApiKey,
16164
16669
  telemetryLevel,
16670
+ // Whether the user has ever actually answered (ADR-299 Decision 4).
16671
+ // `telemetryLevel` above deliberately collapses the first-run 'unset'
16672
+ // sentinel to 'off' so every consumer fails safe — but that also makes
16673
+ // "never asked" indistinguishable from "chose Off", and the first-run
16674
+ // consent card needs exactly that distinction. Hence a separate flag
16675
+ // rather than leaking 'unset' into the level enum.
16676
+ telemetryDecided: cfg2.telemetry.level !== "unset",
16165
16677
  modelDefaults: cfg2.modelDefaults,
16166
16678
  comfyui: cfg2.comfyui,
16167
16679
  gateway: cfg2.gateway,
@@ -16199,7 +16711,12 @@ function settingsPayload(d) {
16199
16711
  }
16200
16712
  function telemetryPreview(level, version2) {
16201
16713
  if (level === "off") {
16202
- return { level, sends: false, note: "Telemetry is off. Nothing is collected or sent.", payload: null };
16714
+ return {
16715
+ level,
16716
+ sends: true,
16717
+ note: "Telemetry is off. The only thing ever sent is this one-time record of your choice \u2014 no machine id, no hardware, no timestamp. Nothing else, ever.",
16718
+ payload: [{ schema: TELEMETRY_SCHEMA_VERSION, event: "consent_choice", level: "off" }]
16719
+ };
16203
16720
  }
16204
16721
  const sys = getSysInfo();
16205
16722
  const hw = {
@@ -16295,19 +16812,19 @@ function engineInstallDir(eng, enginesRoot) {
16295
16812
  return n.startsWith(norm + "/") || n.startsWith(norm + "\\");
16296
16813
  };
16297
16814
  if (eng.kind === "mlx") {
16298
- const d = join26(enginesRoot, "mlx", "venv");
16815
+ const d = join28(enginesRoot, "mlx", "venv");
16299
16816
  return inside(d) ? d : null;
16300
16817
  }
16301
16818
  if (eng.kind === "rapid-mlx") {
16302
- const d = join26(enginesRoot, "rapid-mlx", "venv");
16819
+ const d = join28(enginesRoot, "rapid-mlx", "venv");
16303
16820
  return inside(d) ? d : null;
16304
16821
  }
16305
16822
  if (eng.kind === "vllm") {
16306
- const d = join26(enginesRoot, "vllm", "venv");
16823
+ const d = join28(enginesRoot, "vllm", "venv");
16307
16824
  return inside(d) ? d : null;
16308
16825
  }
16309
16826
  if (/[\\/]engines[\\/]turboquant[\\/]/.test(eng.binPath)) {
16310
- const d = join26(enginesRoot, "turboquant");
16827
+ const d = join28(enginesRoot, "turboquant");
16311
16828
  return inside(d) ? d : null;
16312
16829
  }
16313
16830
  if (eng.kind === "koboldcpp") {
@@ -16322,7 +16839,7 @@ function engineInstallDir(eng, enginesRoot) {
16322
16839
  if (srcDir) return inside(srcDir) ? srcDir : null;
16323
16840
  const llamaMatch = eng.binPath.replace(/\\/g, "/").match(/\/(llama\.cpp-[^/]+)\//i);
16324
16841
  if (llamaMatch) {
16325
- const d = join26(enginesRoot, llamaMatch[1]);
16842
+ const d = join28(enginesRoot, llamaMatch[1]);
16326
16843
  return inside(d) ? d : null;
16327
16844
  }
16328
16845
  return null;
@@ -16340,7 +16857,7 @@ function cleanModelName(p) {
16340
16857
  function readTail2(path, n) {
16341
16858
  if (!path || !existsSync22(path)) return [];
16342
16859
  try {
16343
- const lines = readFileSync11(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
16860
+ const lines = readFileSync14(path, "utf8").replace(/[\r\n]+$/, "").split("\n").map((l) => l.replace(/\r$/, ""));
16344
16861
  return lines.length > n ? lines.slice(-n) : lines;
16345
16862
  } catch {
16346
16863
  return [];
@@ -16530,7 +17047,7 @@ function buildConnectSnippets(cli, base2, apiKey, modelName, modelKey = modelNam
16530
17047
  }
16531
17048
 
16532
17049
  // src/chat/chat-agent-routes.ts
16533
- import { randomUUID as randomUUID8 } from "crypto";
17050
+ import { randomUUID as randomUUID9 } from "crypto";
16534
17051
  function err4(c, status, code, message) {
16535
17052
  return c.json({ error: { code, message } }, status);
16536
17053
  }
@@ -16580,7 +17097,7 @@ function registerChatAgentRoutes(app2, d) {
16580
17097
  return err4(c, 400, "too_many_agents", `Agent limit reached (${AGENT_CAP}).`);
16581
17098
  }
16582
17099
  const agent = {
16583
- id: randomUUID8(),
17100
+ id: randomUUID9(),
16584
17101
  name: b.name.trim(),
16585
17102
  description: b.description?.trim() ?? "",
16586
17103
  systemPrompt: b.systemPrompt ?? "",
@@ -16744,7 +17261,7 @@ import { streamSSE as streamSSE3 } from "hono/streaming";
16744
17261
  import { EventEmitter as EventEmitter2 } from "events";
16745
17262
 
16746
17263
  // src/code/code-session.ts
16747
- import { join as join29, resolve as resolve3 } from "path";
17264
+ import { join as join31, resolve as resolve3 } from "path";
16748
17265
  import { readFile as readFile2 } from "fs/promises";
16749
17266
  import { Type, Unsafe } from "typebox";
16750
17267
  import {
@@ -16973,7 +17490,7 @@ Cannot execute bash commands.`);
16973
17490
 
16974
17491
  // src/code/containment.ts
16975
17492
  import { realpathSync as realpathSync2 } from "fs";
16976
- import { resolve as resolve2, dirname as dirname10, basename as basename4, join as join27, sep } from "path";
17493
+ import { resolve as resolve2, dirname as dirname10, basename as basename4, join as join29, sep } from "path";
16977
17494
  var isWin = process.platform === "win32";
16978
17495
  function canonicalize(input) {
16979
17496
  if (typeof input !== "string" || input.length === 0) return null;
@@ -16989,7 +17506,7 @@ function canonicalize(input) {
16989
17506
  for (let i = 0; i < 4096; i++) {
16990
17507
  try {
16991
17508
  const real = realpathSync2(cur);
16992
- const full = tail.length ? join27(real, ...[...tail].reverse()) : real;
17509
+ const full = tail.length ? join29(real, ...[...tail].reverse()) : real;
16993
17510
  return normForCompare(full);
16994
17511
  } catch {
16995
17512
  const parent = dirname10(cur);
@@ -17032,8 +17549,8 @@ function isContainedFromRoot(input, root) {
17032
17549
  }
17033
17550
 
17034
17551
  // src/code/persona.ts
17035
- import { readFileSync as readFileSync12 } from "fs";
17036
- import { join as join28 } from "path";
17552
+ import { readFileSync as readFileSync15 } from "fs";
17553
+ import { join as join30 } from "path";
17037
17554
  var MAX_SKILL_CATALOG_CHARS = 16e3;
17038
17555
  var MAX_AGENTS_MD_CHARS = 2e4;
17039
17556
  var MAX_AGENTS_MD_CANDIDATES = 8;
@@ -17151,7 +17668,7 @@ function skillCatalogBlock(skills) {
17151
17668
  }
17152
17669
  function readAgentsFile(path) {
17153
17670
  try {
17154
- const text = readFileSync12(path, "utf8").trim();
17671
+ const text = readFileSync15(path, "utf8").trim();
17155
17672
  if (!text) return null;
17156
17673
  return text.length > MAX_AGENTS_MD_CHARS ? text.slice(0, MAX_AGENTS_MD_CHARS) : text;
17157
17674
  } catch {
@@ -17161,7 +17678,7 @@ function readAgentsFile(path) {
17161
17678
  function resolveAgentsFile(root, candidates) {
17162
17679
  for (const entry of candidates.slice(0, MAX_AGENTS_MD_CANDIDATES)) {
17163
17680
  if (!isContainedFromRoot(entry, root)) continue;
17164
- const text = readAgentsFile(join28(root, entry));
17681
+ const text = readAgentsFile(join30(root, entry));
17165
17682
  if (text !== null) return { name: entry, text };
17166
17683
  }
17167
17684
  return null;
@@ -17692,7 +18209,7 @@ async function runCodeSession(params) {
17692
18209
  console.warn(`[code-session] history seed for session ${sessionId} produced ${sessionManager.getEntries().length}/${expectedEntries} expected entries \u2014 falling back to a plain-text history prefix on this turn's prompt`);
17693
18210
  task = `${HISTORY_FALLBACK_PREFIX}${renderHistoryFallbackText(summaryText, priorMessages)}${HISTORY_FALLBACK_SUFFIX}${rawTask}`;
17694
18211
  }
17695
- const agentDir = join29(d.store.dir(), "pi-agent");
18212
+ const agentDir = join31(d.store.dir(), "pi-agent");
17696
18213
  modelRegistry.registerProvider("local", {
17697
18214
  baseUrl: `${target}/v1`,
17698
18215
  apiKey: "agent-key",
@@ -17823,7 +18340,7 @@ async function runCodeSession(params) {
17823
18340
  await sink({ event: "tool_call", data });
17824
18341
  });
17825
18342
  };
17826
- const skillAgentDir = join29(d.store.dir(), "pi-agent");
18343
+ const skillAgentDir = join31(d.store.dir(), "pi-agent");
17827
18344
  const skillResourceLoader = new DefaultResourceLoader({
17828
18345
  cwd: repoRoot,
17829
18346
  agentDir: skillAgentDir,
@@ -17966,7 +18483,7 @@ async function runCodeSession(params) {
17966
18483
  await sink({ event: "tool_call", data });
17967
18484
  });
17968
18485
  };
17969
- const subAgentDir = join29(d.store.dir(), "pi-agent");
18486
+ const subAgentDir = join31(d.store.dir(), "pi-agent");
17970
18487
  const subResourceLoader = new DefaultResourceLoader({
17971
18488
  cwd: repoRoot,
17972
18489
  agentDir: subAgentDir,
@@ -18524,7 +19041,7 @@ async function compactCodeSession(params) {
18524
19041
  return payload;
18525
19042
  });
18526
19043
  };
18527
- const compactAgentDir = join29(d.store.dir(), "pi-agent");
19044
+ const compactAgentDir = join31(d.store.dir(), "pi-agent");
18528
19045
  const compactResourceLoader = new DefaultResourceLoader({
18529
19046
  cwd: repoRoot,
18530
19047
  agentDir: compactAgentDir,
@@ -18626,7 +19143,7 @@ async function lookbackPreCompactionHistory(params) {
18626
19143
  return payload;
18627
19144
  });
18628
19145
  };
18629
- const lookbackAgentDir = join29(d.store.dir(), "pi-agent");
19146
+ const lookbackAgentDir = join31(d.store.dir(), "pi-agent");
18630
19147
  const lookbackResourceLoader = new DefaultResourceLoader({
18631
19148
  cwd: repoRoot,
18632
19149
  agentDir: lookbackAgentDir,
@@ -19073,7 +19590,7 @@ function subscribeToBuffer(buffer, emitter, opts) {
19073
19590
  }
19074
19591
 
19075
19592
  // src/code/revert.ts
19076
- import { existsSync as existsSync23, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "fs";
19593
+ import { existsSync as existsSync23, readFileSync as readFileSync16, writeFileSync as writeFileSync15 } from "fs";
19077
19594
  import { resolve as resolve4 } from "path";
19078
19595
  import { applyPatch, parsePatch, reversePatch } from "diff";
19079
19596
  function revertFileEdits(messages, repoRoot) {
@@ -19102,7 +19619,7 @@ function revertFileEdits(messages, repoRoot) {
19102
19619
  }
19103
19620
  let content;
19104
19621
  try {
19105
- content = readFileSync13(absPath, "utf8");
19622
+ content = readFileSync16(absPath, "utf8");
19106
19623
  } catch {
19107
19624
  failed.push(path);
19108
19625
  continue;
@@ -19131,7 +19648,7 @@ function revertFileEdits(messages, repoRoot) {
19131
19648
  continue;
19132
19649
  }
19133
19650
  try {
19134
- writeFileSync13(absPath, content);
19651
+ writeFileSync15(absPath, content);
19135
19652
  reverted.push(path);
19136
19653
  } catch {
19137
19654
  failed.push(path);
@@ -19899,11 +20416,11 @@ function registerCodeRoutes(app2, d) {
19899
20416
  }
19900
20417
 
19901
20418
  // src/gateway/gateway.ts
19902
- import { randomUUID as randomUUID10 } from "crypto";
20419
+ import { randomUUID as randomUUID11 } from "crypto";
19903
20420
  import { streamSSE as streamSSE4 } from "hono/streaming";
19904
20421
 
19905
20422
  // src/gateway/anthropic.ts
19906
- import { randomUUID as randomUUID9 } from "crypto";
20423
+ import { randomUUID as randomUUID10 } from "crypto";
19907
20424
  var UNSUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
19908
20425
  "format",
19909
20426
  "pattern",
@@ -20072,7 +20589,7 @@ function mapFromOpenAI(oai, modelName) {
20072
20589
  }
20073
20590
  const usage = oai.usage;
20074
20591
  return {
20075
- id: `msg_${randomUUID9().replace(/-/g, "")}`,
20592
+ id: `msg_${randomUUID10().replace(/-/g, "")}`,
20076
20593
  type: "message",
20077
20594
  role: "assistant",
20078
20595
  model: modelName,
@@ -20368,7 +20885,7 @@ function registerGateway(app2, d) {
20368
20885
  );
20369
20886
  }
20370
20887
  if (req.stream) {
20371
- const msgId = `msg_${randomUUID10().replace(/-/g, "")}`;
20888
+ const msgId = `msg_${randomUUID11().replace(/-/g, "")}`;
20372
20889
  const gen = streamToAnthropic(
20373
20890
  res.body,
20374
20891
  modelName,
@@ -20640,9 +21157,28 @@ async function recordOpenAiStreamUsage(d, body7, source, modelKey, codeSessionId
20640
21157
  }
20641
21158
  }
20642
21159
 
21160
+ // src/telemetry/feature-map.ts
21161
+ var BY_SEGMENT = {
21162
+ chat: "chat",
21163
+ code: "code",
21164
+ artifacts: "artifacts",
21165
+ mcp: "mcp",
21166
+ bench: "autotune",
21167
+ // Covers install/uninstall (explicit user setup) AND acquire/release (pushed
21168
+ // by ComfyUI's OWN gate node when an actual generation runs, not polled by
21169
+ // our UI) — unlike skills/chat-agents, every hit here is real image-gen
21170
+ // activity, not a side effect of an unrelated screen loading.
21171
+ comfyui: "image"
21172
+ };
21173
+ function featureForPath(path) {
21174
+ const m = /^\/api\/v1\/([^/?]+)/.exec(path);
21175
+ if (m === null) return null;
21176
+ return BY_SEGMENT[m[1]] ?? null;
21177
+ }
21178
+
20643
21179
  // src/server.ts
20644
21180
  setGlobalDispatcher(new Agent({ keepAliveMaxTimeout: 6e4, connections: 10 }));
20645
- var WEB_ROOT = join30(dirname11(fileURLToPath(import.meta.url)), "webdist");
21181
+ var WEB_ROOT = join32(dirname11(fileURLToPath(import.meta.url)), "webdist");
20646
21182
  function createApp(d) {
20647
21183
  const app2 = new Hono();
20648
21184
  app2.use(
@@ -20667,6 +21203,11 @@ function createApp(d) {
20667
21203
  });
20668
21204
  app2.use("*", lanAuth(d));
20669
21205
  app2.use("/api/v1/code/*", codeAuth(d));
21206
+ app2.use("*", async (c, next) => {
21207
+ const feature = d.telemetry ? featureForPath(c.req.path) : null;
21208
+ if (feature !== null) d.telemetry?.firstUse(feature);
21209
+ await next();
21210
+ });
20670
21211
  app2.get("/healthz", (c) => c.json({ status: "ok", version: d.version }));
20671
21212
  registerApi(app2, d);
20672
21213
  registerChatRoutes(app2, d);
@@ -20680,12 +21221,12 @@ function createApp(d) {
20680
21221
  if (path.startsWith("api/") || path.startsWith("v1/")) {
20681
21222
  return c.json({ error: { code: "not_found", message: "Unknown endpoint." } }, 404);
20682
21223
  }
20683
- let file = normalize2(join30(WEB_ROOT, path || "index.html"));
21224
+ let file = normalize2(join32(WEB_ROOT, path || "index.html"));
20684
21225
  if (!file.startsWith(WEB_ROOT) || !existsSync24(file) || statSync6(file).isDirectory()) {
20685
- file = join30(WEB_ROOT, "index.html");
21226
+ file = join32(WEB_ROOT, "index.html");
20686
21227
  }
20687
21228
  if (!existsSync24(file)) return c.text("web ui not built \u2014 run `npm run build:web`", 500);
20688
- return new Response(readFileSync14(file), { status: 200, headers: { "Content-Type": contentType(file) } });
21229
+ return new Response(readFileSync17(file), { status: 200, headers: { "Content-Type": contentType(file) } });
20689
21230
  });
20690
21231
  return app2;
20691
21232
  }
@@ -20711,21 +21252,21 @@ function contentType(file) {
20711
21252
 
20712
21253
  // src/tunnel/manager.ts
20713
21254
  import { execFile as execFile13, spawn as spawn7, spawnSync as spawnSync3 } from "child_process";
20714
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync15, readdirSync as readdirSync10, rmSync as rmSync15, writeFileSync as writeFileSync14 } from "fs";
20715
- import { join as join32 } from "path";
21255
+ import { mkdirSync as mkdirSync18, readFileSync as readFileSync18, readdirSync as readdirSync11, rmSync as rmSync16, writeFileSync as writeFileSync16 } from "fs";
21256
+ import { join as join34 } from "path";
20716
21257
 
20717
21258
  // src/tunnel/provision.ts
20718
- import { chmodSync as chmodSync3, existsSync as existsSync25, mkdirSync as mkdirSync15, rmSync as rmSync14 } from "fs";
20719
- import { join as join31 } from "path";
21259
+ import { chmodSync as chmodSync3, existsSync as existsSync25, mkdirSync as mkdirSync17, rmSync as rmSync15 } from "fs";
21260
+ import { join as join33 } from "path";
20720
21261
  var CLOUDFLARED_REPO = "cloudflare/cloudflared";
20721
21262
  function cloudflaredDir(dataDir) {
20722
- return join31(dataDir, "cloudflared");
21263
+ return join33(dataDir, "cloudflared");
20723
21264
  }
20724
21265
  function cloudflaredBinName(platform = process.platform) {
20725
21266
  return platform === "win32" ? "cloudflared.exe" : "cloudflared";
20726
21267
  }
20727
21268
  function cloudflaredBinPath(dataDir, platform = process.platform) {
20728
- return join31(cloudflaredDir(dataDir), cloudflaredBinName(platform));
21269
+ return join33(cloudflaredDir(dataDir), cloudflaredBinName(platform));
20729
21270
  }
20730
21271
  function cloudflaredAssetName(platform = process.platform, archStr = process.arch) {
20731
21272
  if (platform === "win32") {
@@ -20754,18 +21295,18 @@ async function ensureCloudflared(dataDir, onProgress, signal) {
20754
21295
  if (existsSync25(binPath)) return { binPath, version: version2 };
20755
21296
  const asset = pickCloudflaredAsset(rel.assets ?? []);
20756
21297
  if (!asset) throw new Error("no_release_asset");
20757
- mkdirSync15(dir, { recursive: true });
21298
+ mkdirSync17(dir, { recursive: true });
20758
21299
  onProgress?.({ phase: "downloading", pct: 0 });
20759
21300
  if (asset.name.endsWith(".tgz")) {
20760
- const tmp = join31(dir, asset.name);
21301
+ const tmp = join33(dir, asset.name);
20761
21302
  try {
20762
21303
  await downloadFile(asset.browser_download_url, tmp, onProgress, signal);
20763
21304
  onProgress?.({ phase: "extracting", pct: -1 });
20764
21305
  await extractArchive(tmp, dir);
20765
- rmSync14(tmp, { force: true });
21306
+ rmSync15(tmp, { force: true });
20766
21307
  } catch (e) {
20767
- rmSync14(tmp, { force: true });
20768
- rmSync14(dir, { recursive: true, force: true });
21308
+ rmSync15(tmp, { force: true });
21309
+ rmSync15(dir, { recursive: true, force: true });
20769
21310
  throw e;
20770
21311
  }
20771
21312
  const found = findFile(dir, "cloudflared");
@@ -20801,21 +21342,21 @@ function forceKill2(child) {
20801
21342
  else child.kill("SIGKILL");
20802
21343
  }
20803
21344
  function tunnelPidDir(dataDir) {
20804
- return join32(dataDir, "run");
21345
+ return join34(dataDir, "run");
20805
21346
  }
20806
21347
  function tunnelPidFile(dataDir, pid) {
20807
- return join32(tunnelPidDir(dataDir), `tunnel-${pid}.pid`);
21348
+ return join34(tunnelPidDir(dataDir), `tunnel-${pid}.pid`);
20808
21349
  }
20809
21350
  function writeTunnelPid(dataDir, pid) {
20810
21351
  try {
20811
- mkdirSync16(tunnelPidDir(dataDir), { recursive: true });
20812
- writeFileSync14(tunnelPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
21352
+ mkdirSync18(tunnelPidDir(dataDir), { recursive: true });
21353
+ writeFileSync16(tunnelPidFile(dataDir, pid), JSON.stringify({ pid, owner: process.pid }));
20813
21354
  } catch {
20814
21355
  }
20815
21356
  }
20816
21357
  function clearTunnelPid(dataDir, pid) {
20817
21358
  try {
20818
- rmSync15(tunnelPidFile(dataDir, pid), { force: true });
21359
+ rmSync16(tunnelPidFile(dataDir, pid), { force: true });
20819
21360
  } catch {
20820
21361
  }
20821
21362
  }
@@ -20832,20 +21373,20 @@ function readTunnelPidFiles(dataDir) {
20832
21373
  const dir = tunnelPidDir(dataDir);
20833
21374
  let names;
20834
21375
  try {
20835
- names = readdirSync10(dir).filter((n) => /^tunnel-\d+\.pid$/.test(n));
21376
+ names = readdirSync11(dir).filter((n) => /^tunnel-\d+\.pid$/.test(n));
20836
21377
  } catch {
20837
21378
  return [];
20838
21379
  }
20839
21380
  const out = [];
20840
21381
  for (const name of names) {
20841
- const file = join32(dir, name);
21382
+ const file = join34(dir, name);
20842
21383
  try {
20843
- const { pid, owner } = JSON.parse(readFileSync15(file, "utf8"));
21384
+ const { pid, owner } = JSON.parse(readFileSync18(file, "utf8"));
20844
21385
  if (typeof pid === "number" && pid > 0) out.push({ pid, owner: typeof owner === "number" ? owner : 0, file });
20845
- else rmSync15(file, { force: true });
21386
+ else rmSync16(file, { force: true });
20846
21387
  } catch {
20847
21388
  try {
20848
- rmSync15(file, { force: true });
21389
+ rmSync16(file, { force: true });
20849
21390
  } catch {
20850
21391
  }
20851
21392
  }
@@ -20863,7 +21404,7 @@ function reapStaleTunnels(dataDir) {
20863
21404
  } catch {
20864
21405
  }
20865
21406
  try {
20866
- rmSync15(file, { force: true });
21407
+ rmSync16(file, { force: true });
20867
21408
  } catch {
20868
21409
  }
20869
21410
  }
@@ -20878,7 +21419,7 @@ function killTrackedTunnelsSync(dataDir) {
20878
21419
  } catch {
20879
21420
  }
20880
21421
  try {
20881
- rmSync15(file, { force: true });
21422
+ rmSync16(file, { force: true });
20882
21423
  } catch {
20883
21424
  }
20884
21425
  }
@@ -20967,12 +21508,94 @@ var TunnelManager = class {
20967
21508
  }
20968
21509
  };
20969
21510
 
21511
+ // src/telemetry/emit.ts
21512
+ import { randomUUID as randomUUID12 } from "crypto";
21513
+ var REQUIRES_FULL = /* @__PURE__ */ new Set(["error"]);
21514
+ var Emitter = class {
21515
+ o;
21516
+ today;
21517
+ constructor(opts) {
21518
+ this.o = opts;
21519
+ this.today = opts.today ?? (() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
21520
+ }
21521
+ /**
21522
+ * Whether this event may be sent right now — kill switch, consent level, and
21523
+ * the per-event level requirement.
21524
+ *
21525
+ * Public because the once-only helpers MUST consult it before spending a
21526
+ * ledger claim. Claiming first would mean a user who browses the app before
21527
+ * answering the consent card burns every `first_use` key while telemetry is
21528
+ * off, and opting in afterwards would permanently lose the whole
21529
+ * feature-discovery picture — the exact data this system exists to collect.
21530
+ */
21531
+ canSend(event) {
21532
+ if (telemetryDisabled()) return false;
21533
+ const level = this.o.store.snapshot().telemetry.level;
21534
+ if (level !== "anon" && level !== "full") return false;
21535
+ if (REQUIRES_FULL.has(event) && level !== "full") return false;
21536
+ return true;
21537
+ }
21538
+ /** Emit one event. Silently does nothing whenever it must not send. */
21539
+ emit(event, payload) {
21540
+ try {
21541
+ if (!this.canSend(event)) return;
21542
+ enqueue(this.o.dataDir, {
21543
+ schema: TELEMETRY_SCHEMA_VERSION,
21544
+ event,
21545
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
21546
+ machineId: this.machineId(),
21547
+ app: { version: this.o.version, os: this.o.os },
21548
+ ...payload === void 0 ? {} : { payload }
21549
+ });
21550
+ } catch {
21551
+ }
21552
+ }
21553
+ /** Emit an event at most once for the lifetime of this install (e.g.
21554
+ * `app_first_run`, which must count installs — not daemon starts). */
21555
+ once(event) {
21556
+ if (!this.canSend(event)) return;
21557
+ if (this.claim(`once:${event}`)) this.emit(event);
21558
+ }
21559
+ /** Emit `feature_first_use` the first time a feature is touched, ever. */
21560
+ firstUse(feature) {
21561
+ if (!this.canSend("feature_first_use")) return;
21562
+ if (this.claim(`first_use:${feature}`)) this.emit("feature_first_use", { feature });
21563
+ }
21564
+ /** Emit `daily_active` at most once per calendar day. */
21565
+ dailyActive() {
21566
+ if (!this.canSend("daily_active")) return;
21567
+ if (this.claim(`daily:${this.today()}`)) this.emit("daily_active");
21568
+ }
21569
+ /**
21570
+ * Mint the anonymous machine id lazily.
21571
+ *
21572
+ * Never called while consent is off, because `emit` returns before this
21573
+ * point — an install that never opts in must not even have an id generated
21574
+ * for it.
21575
+ */
21576
+ machineId() {
21577
+ const existing = this.o.store.snapshot().telemetry.machineId;
21578
+ if (existing) return existing;
21579
+ const id = randomUUID12();
21580
+ this.o.store.update((c) => {
21581
+ if (!c.telemetry.machineId) c.telemetry.machineId = id;
21582
+ });
21583
+ return id;
21584
+ }
21585
+ /** Record a once-only key. Returns whether THIS call claimed it (i.e. whether
21586
+ * the caller should go on to emit). Shared with the consent ping so both
21587
+ * agree on what "once" means. */
21588
+ claim(key) {
21589
+ return claimOnce(this.o.dataDir, key);
21590
+ }
21591
+ };
21592
+
20970
21593
  // src/cli.ts
20971
21594
  hideChildConsoleWindows();
20972
21595
  var version = "0.1.1";
20973
21596
  try {
20974
- const pkgPath = join33(dirname12(fileURLToPath2(import.meta.url)), "..", "package.json");
20975
- version = JSON.parse(readFileSync16(pkgPath, "utf8")).version ?? version;
21597
+ const pkgPath = join35(dirname12(fileURLToPath2(import.meta.url)), "..", "package.json");
21598
+ version = JSON.parse(readFileSync19(pkgPath, "utf8")).version ?? version;
20976
21599
  } catch {
20977
21600
  }
20978
21601
  var [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
@@ -20997,9 +21620,10 @@ function argValue(name, fallback) {
20997
21620
  const i = process.argv.indexOf(name);
20998
21621
  return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback;
20999
21622
  }
21623
+ if (hasFlag("--no-telemetry")) process.env[TELEMETRY_ENV] = "off";
21000
21624
  function configuredDaemonPort(configPath) {
21001
21625
  try {
21002
- const cfg2 = JSON.parse(readFileSync16(configPath, "utf8"));
21626
+ const cfg2 = JSON.parse(readFileSync19(configPath, "utf8"));
21003
21627
  if (typeof cfg2.daemon?.port === "number" && cfg2.daemon.port > 0) return cfg2.daemon.port;
21004
21628
  } catch {
21005
21629
  }
@@ -21073,6 +21697,9 @@ Options:
21073
21697
  tunnel (Cloud Launch) \u2014 prints the public URL + a required
21074
21698
  access token. For running TurboLLM on a rented cloud GPU box.
21075
21699
  --config <f> Path to a custom config file
21700
+ --no-telemetry Disable telemetry entirely, whatever this install's saved
21701
+ setting says (same as TURBOLLM_TELEMETRY=off). For CI,
21702
+ containers, and scripted daemons.
21076
21703
  --stop Stop a running TurboLLM daemon and exit
21077
21704
  --help, -h Show this help message
21078
21705
 
@@ -21119,7 +21746,7 @@ if (pruned > 0) console.log(`pruned ${pruned} dangling engine build(s)`);
21119
21746
  var provision = new ProvisionState();
21120
21747
  var build = new BuildState();
21121
21748
  var updates = new UpdateChecker();
21122
- var enginesDir = join33(store.dir(), "engines");
21749
+ var enginesDir = join35(store.dir(), "engines");
21123
21750
  void seedDefaultEngines(registry, enginesDir, provision).then(() => registry.ensureProbed());
21124
21751
  var manager = new Manager(store);
21125
21752
  var scanner = new Scanner(store);
@@ -21148,6 +21775,19 @@ var appUpdates = new AppUpdateChecker(version);
21148
21775
  var deps = { store, registry, manager, scanner, hashes, db, provision, build, updates, appUpdates, hf, downloads, bench, modelRouter, comfy, tools: toolRegistry, version, startedAt };
21149
21776
  deps.gate = new GenerationGate();
21150
21777
  deps.agentTasks = new AgentTaskState();
21778
+ var telemetry = new Emitter({ dataDir: store.dir(), store, version, os: getSysInfo().os });
21779
+ deps.telemetry = telemetry;
21780
+ telemetry.dailyActive();
21781
+ manager.onLoadSettled = (ok, err7) => reportModelLoad(store.dir(), telemetry, ok ? "ok" : "fail", ok ? void 0 : classifyLoadFailure(err7));
21782
+ bench.telemetry = telemetry;
21783
+ build.onSettled = (ok) => telemetry.emit("onboarding_step", { step: "engine_build", outcome: ok ? "ok" : "fail" });
21784
+ provision.onSettled = (ok) => telemetry.emit("onboarding_step", { step: "engine_install", outcome: ok ? "ok" : "fail" });
21785
+ downloads.onSettled = (outcome) => telemetry.emit("onboarding_step", { step: "model_download", outcome });
21786
+ setTimeout(() => {
21787
+ telemetry.once("app_first_run");
21788
+ void flush(store.dir(), store.snapshot().telemetry.level);
21789
+ }, 3e3).unref();
21790
+ setInterval(() => void flush(store.dir(), store.snapshot().telemetry.level), 5 * 6e4).unref();
21151
21791
  var tunnelRequested = hasFlag("--tunnel");
21152
21792
  if (tunnelRequested) deps.tunnel = new TunnelManager(store.dir());
21153
21793
  var app = createApp(deps);
@@ -21293,7 +21933,7 @@ var restarting = false;
21293
21933
  function spawnReplacement() {
21294
21934
  let out = "ignore";
21295
21935
  try {
21296
- out = openSync3(join33(store.dir(), "restart.log"), "a");
21936
+ out = openSync3(join35(store.dir(), "restart.log"), "a");
21297
21937
  } catch {
21298
21938
  out = "ignore";
21299
21939
  }