hotsheet 0.18.1-beta.3 → 0.19.0-beta.1

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 (2) hide show
  1. package/dist/cli.js +720 -171
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15198,6 +15198,132 @@ var init_zod = __esm({
15198
15198
  }
15199
15199
  });
15200
15200
 
15201
+ // src/hashWorker.ts
15202
+ var hashWorker_exports = {};
15203
+ __export(hashWorker_exports, {
15204
+ _resetHashWorkerForTests: () => _resetHashWorkerForTests,
15205
+ hashFileInProcess: () => hashFileInProcess,
15206
+ hashFileOffThread: () => hashFileOffThread,
15207
+ terminateHashWorker: () => terminateHashWorker
15208
+ });
15209
+ import { createHash } from "crypto";
15210
+ import { createReadStream } from "fs";
15211
+ import { pipeline } from "stream/promises";
15212
+ import { Worker } from "worker_threads";
15213
+ function failWorker(err) {
15214
+ for (const p of pending.values()) p.reject(err);
15215
+ pending.clear();
15216
+ if (worker !== null) {
15217
+ const w = worker;
15218
+ worker = null;
15219
+ void w.terminate().catch(() => {
15220
+ });
15221
+ }
15222
+ crashCount++;
15223
+ if (crashCount >= MAX_WORKER_CRASHES) workerUnusable = true;
15224
+ }
15225
+ function ensureWorker() {
15226
+ if (workerUnusable) return null;
15227
+ if (worker !== null) return worker;
15228
+ try {
15229
+ const w = new Worker(WORKER_SOURCE, { eval: true });
15230
+ w.on("message", (msg) => {
15231
+ const p = pending.get(msg.id);
15232
+ if (p === void 0) return;
15233
+ pending.delete(msg.id);
15234
+ if (msg.error !== void 0) p.reject(new Error(msg.error));
15235
+ else p.resolve({ sha: msg.sha ?? "", size: msg.size ?? 0 });
15236
+ });
15237
+ w.on("error", (err) => {
15238
+ failWorker(err);
15239
+ });
15240
+ w.on("exit", (code) => {
15241
+ if (code !== 0) failWorker(new Error(`hash worker exited with code ${code.toString()}`));
15242
+ });
15243
+ w.unref();
15244
+ worker = w;
15245
+ return w;
15246
+ } catch {
15247
+ workerUnusable = true;
15248
+ return null;
15249
+ }
15250
+ }
15251
+ async function hashFileOffThread(path) {
15252
+ const w = ensureWorker();
15253
+ if (w === null) return hashFileInProcess(path);
15254
+ return new Promise((resolve11, reject2) => {
15255
+ const id = nextId++;
15256
+ pending.set(id, { resolve: resolve11, reject: reject2 });
15257
+ try {
15258
+ w.postMessage({ id, path });
15259
+ } catch (err) {
15260
+ pending.delete(id);
15261
+ reject2(err instanceof Error ? err : new Error(String(err)));
15262
+ }
15263
+ });
15264
+ }
15265
+ async function hashFileInProcess(path) {
15266
+ const hash2 = createHash("sha256");
15267
+ let size = 0;
15268
+ await pipeline(
15269
+ createReadStream(path),
15270
+ async function* (source) {
15271
+ for await (const chunk of source) {
15272
+ size += chunk.length;
15273
+ hash2.update(chunk);
15274
+ yield chunk;
15275
+ }
15276
+ },
15277
+ async function(source) {
15278
+ for await (const _ of source) {
15279
+ }
15280
+ }
15281
+ );
15282
+ return { sha: hash2.digest("hex"), size };
15283
+ }
15284
+ async function terminateHashWorker() {
15285
+ const w = worker;
15286
+ worker = null;
15287
+ pending.clear();
15288
+ if (w !== null) {
15289
+ try {
15290
+ await w.terminate();
15291
+ } catch {
15292
+ }
15293
+ }
15294
+ }
15295
+ function _resetHashWorkerForTests() {
15296
+ void terminateHashWorker();
15297
+ workerUnusable = false;
15298
+ crashCount = 0;
15299
+ pending.clear();
15300
+ }
15301
+ var WORKER_SOURCE, MAX_WORKER_CRASHES, worker, workerUnusable, crashCount, nextId, pending;
15302
+ var init_hashWorker = __esm({
15303
+ "src/hashWorker.ts"() {
15304
+ "use strict";
15305
+ WORKER_SOURCE = `
15306
+ const { parentPort } = require('worker_threads');
15307
+ const { createHash } = require('crypto');
15308
+ const { createReadStream } = require('fs');
15309
+ parentPort.on('message', (msg) => {
15310
+ const hash = createHash('sha256');
15311
+ let size = 0;
15312
+ const stream = createReadStream(msg.path);
15313
+ stream.on('data', (chunk) => { size += chunk.length; hash.update(chunk); });
15314
+ stream.on('end', () => parentPort.postMessage({ id: msg.id, sha: hash.digest('hex'), size: size }));
15315
+ stream.on('error', (err) => parentPort.postMessage({ id: msg.id, error: (err && err.message) ? err.message : String(err) }));
15316
+ });
15317
+ `;
15318
+ MAX_WORKER_CRASHES = 3;
15319
+ worker = null;
15320
+ workerUnusable = false;
15321
+ crashCount = 0;
15322
+ nextId = 1;
15323
+ pending = /* @__PURE__ */ new Map();
15324
+ }
15325
+ });
15326
+
15201
15327
  // src/attachmentBackup.ts
15202
15328
  var attachmentBackup_exports = {};
15203
15329
  __export(attachmentBackup_exports, {
@@ -15214,10 +15340,8 @@ __export(attachmentBackup_exports, {
15214
15340
  runAttachmentGc: () => runAttachmentGc,
15215
15341
  writeManifestAtomically: () => writeManifestAtomically
15216
15342
  });
15217
- import { createHash } from "crypto";
15218
15343
  import {
15219
15344
  copyFileSync,
15220
- createReadStream,
15221
15345
  existsSync,
15222
15346
  mkdirSync,
15223
15347
  promises as fsp,
@@ -15227,7 +15351,6 @@ import {
15227
15351
  statSync
15228
15352
  } from "fs";
15229
15353
  import { join } from "path";
15230
- import { pipeline } from "stream/promises";
15231
15354
  import { gunzipSync } from "zlib";
15232
15355
  function manifestSiblingFilename(tarballFilename) {
15233
15356
  if (!tarballFilename.endsWith(".tar.gz")) {
@@ -15236,24 +15359,7 @@ function manifestSiblingFilename(tarballFilename) {
15236
15359
  return `${tarballFilename.slice(0, -".tar.gz".length)}.attachments.json`;
15237
15360
  }
15238
15361
  async function hashFile(path) {
15239
- const hash2 = createHash("sha256");
15240
- let size = 0;
15241
- await pipeline(
15242
- createReadStream(path),
15243
- async function* (source) {
15244
- for await (const chunk of source) {
15245
- size += chunk.length;
15246
- hash2.update(chunk);
15247
- yield chunk;
15248
- }
15249
- },
15250
- // Sink: just consume the bytes; pipeline needs a writable end.
15251
- async function(source) {
15252
- for await (const _ of source) {
15253
- }
15254
- }
15255
- );
15256
- return { sha: hash2.digest("hex"), size };
15362
+ return hashFileOffThread(path);
15257
15363
  }
15258
15364
  function attachmentBlobsDir(backupRoot) {
15259
15365
  return join(backupRoot, "attachments");
@@ -15400,6 +15506,7 @@ async function runAttachmentGc(backupRoot) {
15400
15506
  const manifestPaths = collectManifestPaths(backupRoot);
15401
15507
  const liveShas = /* @__PURE__ */ new Set();
15402
15508
  let parseFailure = false;
15509
+ let scanned = 0;
15403
15510
  for (const p of manifestPaths) {
15404
15511
  const m = readManifest(p);
15405
15512
  if (m === null) {
@@ -15408,23 +15515,27 @@ async function runAttachmentGc(backupRoot) {
15408
15515
  break;
15409
15516
  }
15410
15517
  for (const e of m.entries) liveShas.add(e.sha);
15518
+ if (++scanned % 25 === 0) await yieldToEventLoop();
15411
15519
  }
15412
15520
  if (parseFailure) {
15413
15521
  return { deleted: 0, bytesReclaimed: 0, scannedManifests: manifestPaths.length, skippedDueToParseFailure: true };
15414
15522
  }
15415
15523
  let deleted = 0;
15416
15524
  let bytesReclaimed = 0;
15417
- for (const name of readdirSync(blobsDir)) {
15525
+ const blobNames = await fsp.readdir(blobsDir);
15526
+ let iterated = 0;
15527
+ for (const name of blobNames) {
15528
+ if (++iterated % 500 === 0) await yieldToEventLoop();
15418
15529
  if (name.endsWith(".tmp")) continue;
15419
15530
  if (liveShas.has(name)) continue;
15420
15531
  const p = join(blobsDir, name);
15421
15532
  let size = 0;
15422
15533
  try {
15423
- size = statSync(p).size;
15534
+ size = (await fsp.stat(p)).size;
15424
15535
  } catch {
15425
15536
  }
15426
15537
  try {
15427
- rmSync(p, { force: true });
15538
+ await fsp.rm(p, { force: true });
15428
15539
  deleted++;
15429
15540
  bytesReclaimed += size;
15430
15541
  } catch (err) {
@@ -15634,6 +15745,7 @@ var init_attachmentBackup = __esm({
15634
15745
  "src/attachmentBackup.ts"() {
15635
15746
  "use strict";
15636
15747
  init_zod();
15748
+ init_hashWorker();
15637
15749
  ATTACHMENT_MANIFEST_VERSION = 1;
15638
15750
  JsonCosaveAttachmentRowSchema = external_exports.object({
15639
15751
  id: external_exports.number(),
@@ -15949,10 +16061,14 @@ __export(freezeLogger_exports, {
15949
16061
  FREEZE_LOG_MAX_BYTES: () => FREEZE_LOG_MAX_BYTES,
15950
16062
  FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE: () => FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE,
15951
16063
  LONG_TASK_THRESHOLD_MS: () => LONG_TASK_THRESHOLD_MS,
16064
+ WAKE_GAP_THRESHOLD_MS: () => WAKE_GAP_THRESHOLD_MS,
15952
16065
  _resetForTesting: () => _resetForTesting,
16066
+ _simulateHeartbeatGapForTesting: () => _simulateHeartbeatGapForTesting,
15953
16067
  appendFreezeLog: () => appendFreezeLog,
16068
+ getRecentEventLoopLagMs: () => getRecentEventLoopLagMs,
15954
16069
  instrumentAsync: () => instrumentAsync,
15955
16070
  instrumentSync: () => instrumentSync,
16071
+ onServerWake: () => onServerWake,
15956
16072
  startServerEventLoopHeartbeat: () => startServerEventLoopHeartbeat,
15957
16073
  stopServerEventLoopHeartbeat: () => stopServerEventLoopHeartbeat
15958
16074
  });
@@ -16011,6 +16127,44 @@ async function rotateIfNeeded(path, pendingBytes) {
16011
16127
  console.warn("[hotsheet freeze.log] rotate writeFile failed:", err instanceof Error ? err.message : String(err));
16012
16128
  }
16013
16129
  }
16130
+ function onServerWake(listener) {
16131
+ wakeListeners.add(listener);
16132
+ return () => {
16133
+ wakeListeners.delete(listener);
16134
+ };
16135
+ }
16136
+ function handleHeartbeatGap(blockMs) {
16137
+ if (blockMs >= WAKE_GAP_THRESHOLD_MS) {
16138
+ lastEventLoopLagMs = 0;
16139
+ if (heartbeatDataDir !== null) {
16140
+ void appendFreezeLog(heartbeatDataDir, {
16141
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
16142
+ source: "server-wake",
16143
+ durationMs: Math.round(blockMs),
16144
+ context: `resumed from suspend after ~${Math.round(blockMs / 1e3).toString()}s`
16145
+ });
16146
+ }
16147
+ for (const listener of wakeListeners) {
16148
+ try {
16149
+ listener(blockMs);
16150
+ } catch {
16151
+ }
16152
+ }
16153
+ return;
16154
+ }
16155
+ lastEventLoopLagMs = blockMs > 0 ? blockMs : 0;
16156
+ if (blockMs >= LONG_TASK_THRESHOLD_MS && heartbeatDataDir !== null) {
16157
+ void appendFreezeLog(heartbeatDataDir, {
16158
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
16159
+ source: "server-heartbeat",
16160
+ durationMs: Math.round(blockMs),
16161
+ context: "event-loop blocked"
16162
+ });
16163
+ }
16164
+ }
16165
+ function _simulateHeartbeatGapForTesting(blockMs) {
16166
+ handleHeartbeatGap(blockMs);
16167
+ }
16014
16168
  function startServerEventLoopHeartbeat(dataDir) {
16015
16169
  if (heartbeatTimer !== null) return;
16016
16170
  heartbeatDataDir = dataDir;
@@ -16019,18 +16173,13 @@ function startServerEventLoopHeartbeat(dataDir) {
16019
16173
  const now = process.hrtime.bigint();
16020
16174
  const elapsedMs = Number(now - lastHeartbeatNs) / 1e6;
16021
16175
  lastHeartbeatNs = now;
16022
- const blockMs = elapsedMs - HEARTBEAT_INTERVAL_MS;
16023
- if (blockMs >= LONG_TASK_THRESHOLD_MS && heartbeatDataDir !== null) {
16024
- void appendFreezeLog(heartbeatDataDir, {
16025
- ts: (/* @__PURE__ */ new Date()).toISOString(),
16026
- source: "server-heartbeat",
16027
- durationMs: Math.round(blockMs),
16028
- context: "event-loop blocked"
16029
- });
16030
- }
16176
+ handleHeartbeatGap(elapsedMs - HEARTBEAT_INTERVAL_MS);
16031
16177
  }, HEARTBEAT_INTERVAL_MS);
16032
16178
  heartbeatTimer.unref();
16033
16179
  }
16180
+ function getRecentEventLoopLagMs() {
16181
+ return lastEventLoopLagMs;
16182
+ }
16034
16183
  function stopServerEventLoopHeartbeat() {
16035
16184
  if (heartbeatTimer !== null) {
16036
16185
  clearInterval(heartbeatTimer);
@@ -16077,9 +16226,11 @@ function _resetForTesting() {
16077
16226
  }
16078
16227
  heartbeatDataDir = null;
16079
16228
  lastHeartbeatNs = 0n;
16229
+ lastEventLoopLagMs = 0;
16230
+ wakeListeners.clear();
16080
16231
  appendQueue.clear();
16081
16232
  }
16082
- var FREEZE_LOG_FILENAME, LONG_TASK_THRESHOLD_MS, FREEZE_LOG_MAX_BYTES, FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE, HEARTBEAT_INTERVAL_MS, appendQueue, heartbeatTimer, lastHeartbeatNs, heartbeatDataDir;
16233
+ var FREEZE_LOG_FILENAME, LONG_TASK_THRESHOLD_MS, FREEZE_LOG_MAX_BYTES, FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE, HEARTBEAT_INTERVAL_MS, WAKE_GAP_THRESHOLD_MS, appendQueue, heartbeatTimer, lastHeartbeatNs, lastEventLoopLagMs, heartbeatDataDir, wakeListeners;
16083
16234
  var init_freezeLogger = __esm({
16084
16235
  "src/diagnostics/freezeLogger.ts"() {
16085
16236
  "use strict";
@@ -16088,10 +16239,205 @@ var init_freezeLogger = __esm({
16088
16239
  FREEZE_LOG_MAX_BYTES = 1048576;
16089
16240
  FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE = 524288;
16090
16241
  HEARTBEAT_INTERVAL_MS = 50;
16242
+ WAKE_GAP_THRESHOLD_MS = 1e4;
16091
16243
  appendQueue = /* @__PURE__ */ new Map();
16092
16244
  heartbeatTimer = null;
16093
16245
  lastHeartbeatNs = 0n;
16246
+ lastEventLoopLagMs = 0;
16094
16247
  heartbeatDataDir = null;
16248
+ wakeListeners = /* @__PURE__ */ new Set();
16249
+ }
16250
+ });
16251
+
16252
+ // src/scheduler/backgroundScheduler.ts
16253
+ var backgroundScheduler_exports = {};
16254
+ __export(backgroundScheduler_exports, {
16255
+ PRIORITY: () => PRIORITY,
16256
+ _resetDefaultSchedulerForTests: () => _resetDefaultSchedulerForTests,
16257
+ createBackgroundScheduler: () => createBackgroundScheduler,
16258
+ getBackgroundScheduler: () => getBackgroundScheduler
16259
+ });
16260
+ function createBackgroundScheduler(opts = {}) {
16261
+ const concurrency = opts.concurrency ?? 2;
16262
+ const lagProvider = opts.lagProvider ?? getRecentEventLoopLagMs;
16263
+ const lagThresholdMs = opts.lagThresholdMs ?? 200;
16264
+ const reDrainDelayMs = opts.reDrainDelayMs ?? 250;
16265
+ const now = opts.now ?? (() => Date.now());
16266
+ const wakeStaggerWindowMs = opts.wakeStaggerWindowMs ?? 15e3;
16267
+ const wakeStaggerStepMs = opts.wakeStaggerStepMs ?? 250;
16268
+ const onError = opts.onError ?? ((key, err) => {
16269
+ console.warn("[hotsheet backgroundScheduler] job failed:", key, err instanceof Error ? err.message : String(err));
16270
+ });
16271
+ const pending2 = /* @__PURE__ */ new Map();
16272
+ const running = /* @__PURE__ */ new Set();
16273
+ const runningGroups = /* @__PURE__ */ new Set();
16274
+ const awaiters = /* @__PURE__ */ new Map();
16275
+ const runningAwaiters = /* @__PURE__ */ new Map();
16276
+ let seq = 0;
16277
+ const lastServedSeq = /* @__PURE__ */ new Map();
16278
+ let reDrainTimer = null;
16279
+ const idleWaiters = [];
16280
+ let staggerUntil = 0;
16281
+ let lastStartAt = Number.NEGATIVE_INFINITY;
16282
+ let staggerTimer = null;
16283
+ function projectKeyOf(job) {
16284
+ return job.projectKey ?? job.key;
16285
+ }
16286
+ function settleIdleIfDone() {
16287
+ if (running.size === 0 && pending2.size === 0 && idleWaiters.length > 0) {
16288
+ const waiters = idleWaiters.splice(0, idleWaiters.length);
16289
+ for (const w of waiters) w();
16290
+ }
16291
+ }
16292
+ function pickNext(highLag) {
16293
+ let best = null;
16294
+ let lagDeferred = false;
16295
+ for (const job of pending2.values()) {
16296
+ if (running.has(job.key)) continue;
16297
+ if (job.exclusiveGroup !== void 0 && runningGroups.has(job.exclusiveGroup)) continue;
16298
+ if (highLag && job.deferUnderLag === true) {
16299
+ lagDeferred = true;
16300
+ continue;
16301
+ }
16302
+ if (best === null) {
16303
+ best = job;
16304
+ continue;
16305
+ }
16306
+ if (job.priority !== best.priority) {
16307
+ if (job.priority < best.priority) best = job;
16308
+ continue;
16309
+ }
16310
+ const a = lastServedSeq.get(projectKeyOf(job)) ?? -1;
16311
+ const b = lastServedSeq.get(projectKeyOf(best)) ?? -1;
16312
+ if (a < b) best = job;
16313
+ }
16314
+ return { job: best, lagDeferred };
16315
+ }
16316
+ function drain() {
16317
+ const highLag = lagProvider() > lagThresholdMs;
16318
+ const t = now();
16319
+ const inStagger = t < staggerUntil;
16320
+ const cap = inStagger ? 1 : concurrency;
16321
+ let armReDrain = false;
16322
+ while (running.size < cap) {
16323
+ if (inStagger) {
16324
+ const sinceLast = t - lastStartAt;
16325
+ if (sinceLast < wakeStaggerStepMs) {
16326
+ scheduleStaggerDrain(wakeStaggerStepMs - sinceLast);
16327
+ break;
16328
+ }
16329
+ }
16330
+ const { job, lagDeferred } = pickNext(highLag);
16331
+ if (job === null) {
16332
+ armReDrain = lagDeferred;
16333
+ break;
16334
+ }
16335
+ pending2.delete(job.key);
16336
+ running.add(job.key);
16337
+ if (job.exclusiveGroup !== void 0) runningGroups.add(job.exclusiveGroup);
16338
+ runningAwaiters.set(job.key, awaiters.get(job.key) ?? []);
16339
+ awaiters.delete(job.key);
16340
+ lastServedSeq.set(projectKeyOf(job), ++seq);
16341
+ lastStartAt = t;
16342
+ void runJob(job);
16343
+ }
16344
+ if (armReDrain) scheduleReDrain();
16345
+ settleIdleIfDone();
16346
+ }
16347
+ async function runJob(job) {
16348
+ try {
16349
+ await job.run();
16350
+ } catch (err) {
16351
+ onError(job.key, err);
16352
+ } finally {
16353
+ running.delete(job.key);
16354
+ if (job.exclusiveGroup !== void 0) runningGroups.delete(job.exclusiveGroup);
16355
+ const settled = runningAwaiters.get(job.key);
16356
+ runningAwaiters.delete(job.key);
16357
+ if (settled !== void 0) for (const r of settled) r();
16358
+ drain();
16359
+ }
16360
+ }
16361
+ function scheduleReDrain() {
16362
+ if (reDrainTimer !== null) return;
16363
+ reDrainTimer = setTimeout(() => {
16364
+ reDrainTimer = null;
16365
+ drain();
16366
+ }, reDrainDelayMs);
16367
+ reDrainTimer.unref();
16368
+ }
16369
+ function scheduleStaggerDrain(delayMs) {
16370
+ if (staggerTimer !== null) clearTimeout(staggerTimer);
16371
+ staggerTimer = setTimeout(() => {
16372
+ staggerTimer = null;
16373
+ drain();
16374
+ }, Math.max(0, delayMs));
16375
+ staggerTimer.unref();
16376
+ }
16377
+ return {
16378
+ submit(job) {
16379
+ pending2.set(job.key, job);
16380
+ const done = new Promise((resolve11) => {
16381
+ const list = awaiters.get(job.key) ?? [];
16382
+ list.push(resolve11);
16383
+ awaiters.set(job.key, list);
16384
+ });
16385
+ drain();
16386
+ return done;
16387
+ },
16388
+ runningCount: () => running.size,
16389
+ pendingCount: () => pending2.size,
16390
+ onIdle() {
16391
+ if (running.size === 0 && pending2.size === 0) return Promise.resolve();
16392
+ return new Promise((resolve11) => {
16393
+ idleWaiters.push(resolve11);
16394
+ });
16395
+ },
16396
+ noteWake() {
16397
+ staggerUntil = now() + wakeStaggerWindowMs;
16398
+ drain();
16399
+ },
16400
+ clear() {
16401
+ pending2.clear();
16402
+ if (reDrainTimer !== null) {
16403
+ clearTimeout(reDrainTimer);
16404
+ reDrainTimer = null;
16405
+ }
16406
+ if (staggerTimer !== null) {
16407
+ clearTimeout(staggerTimer);
16408
+ staggerTimer = null;
16409
+ }
16410
+ staggerUntil = 0;
16411
+ const orphaned = [...awaiters.values()].flat();
16412
+ awaiters.clear();
16413
+ for (const r of orphaned) r();
16414
+ settleIdleIfDone();
16415
+ }
16416
+ };
16417
+ }
16418
+ function getBackgroundScheduler() {
16419
+ if (defaultScheduler === null) {
16420
+ defaultScheduler = createBackgroundScheduler();
16421
+ }
16422
+ return defaultScheduler;
16423
+ }
16424
+ function _resetDefaultSchedulerForTests() {
16425
+ defaultScheduler?.clear();
16426
+ defaultScheduler = null;
16427
+ }
16428
+ var PRIORITY, defaultScheduler;
16429
+ var init_backgroundScheduler = __esm({
16430
+ "src/scheduler/backgroundScheduler.ts"() {
16431
+ "use strict";
16432
+ init_freezeLogger();
16433
+ PRIORITY = {
16434
+ GIT_STATUS: 10,
16435
+ MARKDOWN_SYNC: 20,
16436
+ SNAPSHOT: 30,
16437
+ BACKUP: 40,
16438
+ GC: 50
16439
+ };
16440
+ defaultScheduler = null;
16095
16441
  }
16096
16442
  });
16097
16443
 
@@ -16148,7 +16494,7 @@ function initSnapshotScheduler(dataDir) {
16148
16494
  const state = getOrCreateState(dataDir);
16149
16495
  if (state.safetyTimer !== null) return;
16150
16496
  state.safetyTimer = setInterval(() => {
16151
- if (state.dirty && !state.inProgress) void writeSnapshotNow(dataDir);
16497
+ if (state.dirty && !state.inProgress) void submitSnapshotJob(dataDir);
16152
16498
  }, numericSetting(dataDir, "db_snapshot_safety_interval_ms", DEFAULT_SAFETY_INTERVAL_MS));
16153
16499
  state.safetyTimer.unref();
16154
16500
  }
@@ -16161,9 +16507,20 @@ function scheduleSnapshot(dataDir) {
16161
16507
  if (state.debounceTimer) clearTimeout(state.debounceTimer);
16162
16508
  state.debounceTimer = setTimeout(() => {
16163
16509
  state.debounceTimer = null;
16164
- void writeSnapshotNow(dir);
16510
+ void submitSnapshotJob(dir);
16165
16511
  }, numericSetting(dir, "db_snapshot_debounce_ms", DEFAULT_DEBOUNCE_MS));
16166
16512
  }
16513
+ function submitSnapshotJob(dataDir) {
16514
+ return getBackgroundScheduler().submit({
16515
+ key: `snapshot:${dataDir}`,
16516
+ priority: PRIORITY.SNAPSHOT,
16517
+ projectKey: dataDir,
16518
+ deferUnderLag: false,
16519
+ run: async () => {
16520
+ await writeSnapshotNow(dataDir);
16521
+ }
16522
+ });
16523
+ }
16167
16524
  async function writeSnapshotNow(dataDir) {
16168
16525
  if (!isSnapshotProtectionEnabled(dataDir)) return null;
16169
16526
  if (!existsSync4(join5(dataDir, "db"))) return null;
@@ -16205,7 +16562,7 @@ async function snapshotAllForShutdown() {
16205
16562
  for (const dir of dirs) {
16206
16563
  if (!isSnapshotProtectionEnabled(dir)) continue;
16207
16564
  try {
16208
- await writeSnapshotNow(dir);
16565
+ await submitSnapshotJob(dir);
16209
16566
  } catch (err) {
16210
16567
  console.error(`[snapshot] shutdown snapshot failed for ${dir}:`, err);
16211
16568
  }
@@ -16257,6 +16614,7 @@ var init_snapshot = __esm({
16257
16614
  "use strict";
16258
16615
  init_freezeLogger();
16259
16616
  init_file_settings();
16617
+ init_backgroundScheduler();
16260
16618
  init_connection();
16261
16619
  DEFAULT_DEBOUNCE_MS = 2e3;
16262
16620
  DEFAULT_SAFETY_INTERVAL_MS = 12e4;
@@ -16570,10 +16928,10 @@ async function renameDirWithRetry(from, to) {
16570
16928
  }
16571
16929
  async function completeDeferredRecovery(dbPath) {
16572
16930
  const dataDir = dbPath.replace(/[\\/]db$/, "");
16573
- const pending = readPendingRecovery(dataDir);
16574
- if (pending === null) return null;
16575
- if (pending.attempts > MAX_DEFERRED_RECOVERY_ATTEMPTS) {
16576
- console.error(`[db] deferred recovery gave up after ${String(pending.attempts)} attempts; leaving the corrupt cluster for manual rescue.`);
16931
+ const pending2 = readPendingRecovery(dataDir);
16932
+ if (pending2 === null) return null;
16933
+ if (pending2.attempts > MAX_DEFERRED_RECOVERY_ATTEMPTS) {
16934
+ console.error(`[db] deferred recovery gave up after ${String(pending2.attempts)} attempts; leaving the corrupt cluster for manual rescue.`);
16577
16935
  clearPendingRecovery(dataDir);
16578
16936
  return null;
16579
16937
  }
@@ -16586,7 +16944,7 @@ async function completeDeferredRecovery(dbPath) {
16586
16944
  try {
16587
16945
  await renameDirWithRetry(dbPath, corruptPath);
16588
16946
  } catch (renameErr) {
16589
- writePendingRecovery(dataDir, pending.attempts + 1);
16947
+ writePendingRecovery(dataDir, pending2.attempts + 1);
16590
16948
  console.error(`[db] deferred recovery could not move db/ yet: ${getErrorMessage(renameErr)}`);
16591
16949
  return null;
16592
16950
  }
@@ -16883,6 +17241,22 @@ async function initSchema(db) {
16883
17241
  CREATE INDEX IF NOT EXISTS idx_otel_spans_session_ts ON otel_spans(session_id, start_ts);
16884
17242
  CREATE INDEX IF NOT EXISTS idx_otel_spans_prompt ON otel_spans(prompt_id);
16885
17243
  CREATE INDEX IF NOT EXISTS idx_otel_spans_trace ON otel_spans(trace_id);
17244
+
17245
+ -- HS-8730 (per-ticket cost, time-window correlation) \u2014 records when each
17246
+ -- ticket was actively being worked (its status was 'started'), so the
17247
+ -- per-ticket rollup can attribute api_request cost by timestamp instead of
17248
+ -- only the channelUI prompt marker. Lives in the telemetry DB (this is the
17249
+ -- default/primary project's DB per getTelemetryDb) so the rollup join with
17250
+ -- otel_events is single-DB. Keyed by project_secret (matching otel_events).
17251
+ CREATE TABLE IF NOT EXISTS ticket_work_intervals (
17252
+ id SERIAL PRIMARY KEY,
17253
+ project_secret TEXT NOT NULL,
17254
+ ticket_number TEXT NOT NULL,
17255
+ started_at TIMESTAMPTZ NOT NULL,
17256
+ ended_at TIMESTAMPTZ
17257
+ );
17258
+ CREATE INDEX IF NOT EXISTS idx_twi_secret_ticket ON ticket_work_intervals(project_secret, ticket_number);
17259
+ CREATE INDEX IF NOT EXISTS idx_twi_open ON ticket_work_intervals(project_secret, ticket_number, ended_at);
16886
17260
  `);
16887
17261
  await migrateNoteIds(db);
16888
17262
  }
@@ -16916,7 +17290,7 @@ var init_connection = __esm({
16916
17290
  init_zod();
16917
17291
  init_errorMessage();
16918
17292
  init_pglite();
16919
- SCHEMA_VERSION = 4;
17293
+ SCHEMA_VERSION = 5;
16920
17294
  RECOVERY_MARKER_FILENAME = ".db-recovery-marker.json";
16921
17295
  PENDING_RECOVERY_FILENAME = ".db-pending-recovery.json";
16922
17296
  MAX_DEFERRED_RECOVERY_ATTEMPTS = 3;
@@ -17011,20 +17385,27 @@ function getOrCreateState2(dataDir) {
17011
17385
  }
17012
17386
  return state;
17013
17387
  }
17014
- async function withGlobalBackupLock(fn) {
17015
- while (activeBackup !== null) {
17016
- try {
17017
- await activeBackup;
17018
- } catch {
17388
+ function withGlobalBackupLock(fn) {
17389
+ let settle;
17390
+ let fail;
17391
+ const out = new Promise((resolve11, reject2) => {
17392
+ settle = resolve11;
17393
+ fail = reject2;
17394
+ });
17395
+ void getBackgroundScheduler().submit({
17396
+ key: `backup:${++backupLockSeq}`,
17397
+ priority: PRIORITY.BACKUP,
17398
+ exclusiveGroup: BACKUP_EXCLUSIVE_GROUP,
17399
+ deferUnderLag: false,
17400
+ run: async () => {
17401
+ try {
17402
+ settle(await fn());
17403
+ } catch (e) {
17404
+ fail(e);
17405
+ }
17019
17406
  }
17020
- }
17021
- const p = (async () => fn())();
17022
- activeBackup = p;
17023
- try {
17024
- return await p;
17025
- } finally {
17026
- if (activeBackup === p) activeBackup = null;
17027
- }
17407
+ });
17408
+ return out;
17028
17409
  }
17029
17410
  function backupsDir(dataDir) {
17030
17411
  return getBackupDir(dataDir);
@@ -17288,12 +17669,18 @@ function initBackupScheduler(dataDir) {
17288
17669
  });
17289
17670
  }, 3e4);
17290
17671
  state.attachmentGcInterval = setInterval(() => {
17291
- void instrumentAsync(dataDir, "attachmentBackup.orphanGc:daily", () => runAttachmentGc(backupsDir(dataDir))).then((stats) => {
17292
- if (stats.deleted > 0) {
17293
- console.log(`[attachmentBackup] GC: reclaimed ${stats.deleted} blob(s), ${(stats.bytesReclaimed / 1024 / 1024).toFixed(2)} MB`);
17294
- }
17295
- }).catch((err) => {
17296
- console.error("[attachmentBackup] GC daily run failed:", err);
17672
+ void getBackgroundScheduler().submit({
17673
+ key: `attachment-gc:${dataDir}`,
17674
+ priority: PRIORITY.GC,
17675
+ projectKey: dataDir,
17676
+ deferUnderLag: true,
17677
+ run: () => instrumentAsync(dataDir, "attachmentBackup.orphanGc:daily", () => runAttachmentGc(backupsDir(dataDir))).then((stats) => {
17678
+ if (stats.deleted > 0) {
17679
+ console.log(`[attachmentBackup] GC: reclaimed ${stats.deleted} blob(s), ${(stats.bytesReclaimed / 1024 / 1024).toFixed(2)} MB`);
17680
+ }
17681
+ }).catch((err) => {
17682
+ console.error("[attachmentBackup] GC daily run failed:", err);
17683
+ })
17297
17684
  });
17298
17685
  }, 24 * 60 * 60 * 1e3);
17299
17686
  }
@@ -17312,7 +17699,7 @@ async function triggerMissedBackups(dataDir) {
17312
17699
  await createBackup(dataDir, tier);
17313
17700
  }
17314
17701
  }
17315
- var TIERS, backupStates, activePreviews, activeBackup, VALID_TIERS;
17702
+ var TIERS, backupStates, activePreviews, backupLockSeq, BACKUP_EXCLUSIVE_GROUP, VALID_TIERS;
17316
17703
  var init_backup = __esm({
17317
17704
  "src/backup.ts"() {
17318
17705
  "use strict";
@@ -17323,6 +17710,7 @@ var init_backup = __esm({
17323
17710
  init_dbJsonExport();
17324
17711
  init_freezeLogger();
17325
17712
  init_file_settings();
17713
+ init_backgroundScheduler();
17326
17714
  TIERS = {
17327
17715
  "5min": { intervalMs: 5 * 60 * 1e3, maxAge: 60 * 60 * 1e3, maxCount: 12 },
17328
17716
  "hourly": { intervalMs: 60 * 60 * 1e3, maxAge: 12 * 60 * 60 * 1e3, maxCount: 12 },
@@ -17330,7 +17718,8 @@ var init_backup = __esm({
17330
17718
  };
17331
17719
  backupStates = /* @__PURE__ */ new Map();
17332
17720
  activePreviews = /* @__PURE__ */ new Map();
17333
- activeBackup = null;
17721
+ backupLockSeq = 0;
17722
+ BACKUP_EXCLUSIVE_GROUP = "backup";
17334
17723
  VALID_TIERS = /* @__PURE__ */ new Set(["5min", "hourly", "daily"]);
17335
17724
  }
17336
17725
  });
@@ -17905,6 +18294,47 @@ var init_ticketNumber = __esm({
17905
18294
  }
17906
18295
  });
17907
18296
 
18297
+ // src/db/ticketWorkIntervals.ts
18298
+ async function recordTicketWorkTransition(secret, ticketNumber, status) {
18299
+ if (secret === "" || ticketNumber === "") return;
18300
+ try {
18301
+ const db = await getTelemetryDb();
18302
+ await db.query(
18303
+ `UPDATE ticket_work_intervals SET ended_at = NOW()
18304
+ WHERE project_secret = $1 AND ticket_number = $2 AND ended_at IS NULL`,
18305
+ [secret, ticketNumber]
18306
+ );
18307
+ if (status === "started") {
18308
+ await db.query(
18309
+ `INSERT INTO ticket_work_intervals (project_secret, ticket_number, started_at)
18310
+ VALUES ($1, $2, NOW())`,
18311
+ [secret, ticketNumber]
18312
+ );
18313
+ }
18314
+ } catch (err) {
18315
+ console.warn("[ticketWorkIntervals] failed to record transition:", err instanceof Error ? err.message : String(err));
18316
+ }
18317
+ }
18318
+ async function closeOpenTicketIntervalsForProject(secret) {
18319
+ if (secret === "") return;
18320
+ try {
18321
+ const db = await getTelemetryDb();
18322
+ await db.query(
18323
+ `UPDATE ticket_work_intervals SET ended_at = NOW()
18324
+ WHERE project_secret = $1 AND ended_at IS NULL`,
18325
+ [secret]
18326
+ );
18327
+ } catch (err) {
18328
+ console.warn("[ticketWorkIntervals] failed to close open intervals:", err instanceof Error ? err.message : String(err));
18329
+ }
18330
+ }
18331
+ var init_ticketWorkIntervals = __esm({
18332
+ "src/db/ticketWorkIntervals.ts"() {
18333
+ "use strict";
18334
+ init_connection();
18335
+ }
18336
+ });
18337
+
17908
18338
  // src/db/tickets.ts
17909
18339
  function escapeIlike(value) {
17910
18340
  return value.replace(/[%_\\]/g, "\\$&");
@@ -18037,7 +18467,17 @@ async function updateTicket(id, updates, options) {
18037
18467
  `UPDATE tickets SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
18038
18468
  values
18039
18469
  );
18040
- return result.rows[0] ?? null;
18470
+ const updated = result.rows[0] ?? null;
18471
+ if (updates.status !== void 0) {
18472
+ try {
18473
+ const secret = readFileSettings(getDataDir()).secret;
18474
+ if (secret !== void 0 && secret !== "") {
18475
+ void recordTicketWorkTransition(secret, updated.ticket_number, updates.status);
18476
+ }
18477
+ } catch {
18478
+ }
18479
+ }
18480
+ return updated;
18041
18481
  }
18042
18482
  async function deleteTicket(id) {
18043
18483
  await updateTicket(id, { status: "deleted" });
@@ -18376,9 +18816,11 @@ var QUERYABLE_FIELDS, PRIORITY_ORD, STATUS_ORD, PRIORITY_RANK, STATUS_RANK;
18376
18816
  var init_tickets = __esm({
18377
18817
  "src/db/tickets.ts"() {
18378
18818
  "use strict";
18819
+ init_file_settings();
18379
18820
  init_ticketNumber();
18380
18821
  init_connection();
18381
18822
  init_notes();
18823
+ init_ticketWorkIntervals();
18382
18824
  QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["category", "priority", "status", "title", "details", "up_next", "tags"]);
18383
18825
  PRIORITY_ORD = `CASE priority WHEN 'highest' THEN 1 WHEN 'high' THEN 2 WHEN 'default' THEN 3 WHEN 'low' THEN 4 WHEN 'lowest' THEN 5 ELSE 3 END`;
18384
18826
  STATUS_ORD = `CASE status WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3 WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 ELSE 2 END`;
@@ -19483,7 +19925,14 @@ function scheduleWorklistSync(dir) {
19483
19925
  if (!state) return;
19484
19926
  if (state.worklistTimeout) clearTimeout(state.worklistTimeout);
19485
19927
  state.worklistTimeout = setTimeout(() => {
19486
- void runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncWorklist", () => syncWorklist(state)));
19928
+ state.worklistTimeout = null;
19929
+ void getBackgroundScheduler().submit({
19930
+ key: `markdown-worklist:${state.dataDir}`,
19931
+ priority: PRIORITY.MARKDOWN_SYNC,
19932
+ projectKey: state.dataDir,
19933
+ deferUnderLag: true,
19934
+ run: () => runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncWorklist", () => syncWorklist(state)))
19935
+ });
19487
19936
  }, WORKLIST_SYNC_DEBOUNCE_MS);
19488
19937
  }
19489
19938
  function scheduleOpenTicketsSync(dir) {
@@ -19491,7 +19940,14 @@ function scheduleOpenTicketsSync(dir) {
19491
19940
  if (!state) return;
19492
19941
  if (state.openTicketsTimeout) clearTimeout(state.openTicketsTimeout);
19493
19942
  state.openTicketsTimeout = setTimeout(() => {
19494
- void runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncOpenTickets", () => syncOpenTickets(state)));
19943
+ state.openTicketsTimeout = null;
19944
+ void getBackgroundScheduler().submit({
19945
+ key: `markdown-opentickets:${state.dataDir}`,
19946
+ priority: PRIORITY.MARKDOWN_SYNC,
19947
+ projectKey: state.dataDir,
19948
+ deferUnderLag: true,
19949
+ run: () => runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncOpenTickets", () => syncOpenTickets(state)))
19950
+ });
19495
19951
  }, OPEN_TICKETS_SYNC_DEBOUNCE_MS);
19496
19952
  }
19497
19953
  function scheduleAllSync(dir) {
@@ -19809,6 +20265,7 @@ var init_markdown = __esm({
19809
20265
  init_freezeLogger();
19810
20266
  init_file_settings();
19811
20267
  init_limits();
20268
+ init_backgroundScheduler();
19812
20269
  init_schemas3();
19813
20270
  syncStates = /* @__PURE__ */ new Map();
19814
20271
  defaultDataDir2 = null;
@@ -21365,6 +21822,25 @@ var init_sessionStore = __esm({
21365
21822
  }
21366
21823
  });
21367
21824
 
21825
+ // src/activeProjects.ts
21826
+ function markProjectActive(dataDir) {
21827
+ lastActiveAt.set(dataDir, Date.now());
21828
+ }
21829
+ function isProjectActive(dataDir) {
21830
+ if (lastActiveAt.size === 0) return true;
21831
+ const at = lastActiveAt.get(dataDir);
21832
+ if (at === void 0) return false;
21833
+ return Date.now() - at < ACTIVE_TTL_MS;
21834
+ }
21835
+ var ACTIVE_TTL_MS, lastActiveAt;
21836
+ var init_activeProjects = __esm({
21837
+ "src/activeProjects.ts"() {
21838
+ "use strict";
21839
+ ACTIVE_TTL_MS = 9e4;
21840
+ lastActiveAt = /* @__PURE__ */ new Map();
21841
+ }
21842
+ });
21843
+
21368
21844
  // src/gitignore.ts
21369
21845
  import { execFileSync as execFileSync3 } from "child_process";
21370
21846
  import { appendFileSync as appendFileSync3, existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
@@ -21420,56 +21896,72 @@ var init_gitignore = __esm({
21420
21896
  });
21421
21897
 
21422
21898
  // src/git/status.ts
21423
- import { spawnSync } from "child_process";
21899
+ import { execFile as execFile3 } from "child_process";
21424
21900
  import { join as join23 } from "path";
21901
+ import { promisify as promisify2 } from "util";
21902
+ function bufToStr(v) {
21903
+ if (typeof v === "string") return v;
21904
+ if (v !== void 0) return v.toString();
21905
+ return "";
21906
+ }
21425
21907
  function makeGitInvoker({ timeoutMs, includeStderr = false }) {
21426
- return (args, cwd) => {
21427
- const res = spawnSync("git", args, {
21908
+ return async (args, cwd) => {
21909
+ const opts = {
21428
21910
  cwd,
21429
21911
  encoding: "utf-8",
21430
21912
  timeout: timeoutMs,
21913
+ maxBuffer: GIT_MAX_BUFFER,
21431
21914
  env: {
21432
21915
  ...process.env,
21433
21916
  GIT_TERMINAL_PROMPT: "0",
21434
21917
  GIT_OPTIONAL_LOCKS: "0"
21435
21918
  }
21436
- });
21437
- const stdout = typeof res.stdout === "string" ? res.stdout : "";
21438
- const stderr = typeof res.stderr === "string" ? res.stderr : "";
21439
- return { stdout: includeStderr ? stdout + stderr : stdout, status: res.status };
21919
+ };
21920
+ try {
21921
+ const { stdout, stderr } = await execFileAsync2("git", args, opts);
21922
+ const out = bufToStr(stdout);
21923
+ const err = bufToStr(stderr);
21924
+ return { stdout: includeStderr ? out + err : out, status: 0 };
21925
+ } catch (e) {
21926
+ const errObj = e;
21927
+ const out = bufToStr(errObj.stdout);
21928
+ const err = bufToStr(errObj.stderr);
21929
+ const status = typeof errObj.code === "number" ? errObj.code : null;
21930
+ return { stdout: includeStderr ? out + err : out, status };
21931
+ }
21440
21932
  };
21441
21933
  }
21442
- function getGitStatus(projectRoot3, invoker = defaultInvoker) {
21934
+ async function getGitStatus(projectRoot3, invoker = defaultInvoker) {
21443
21935
  if (!isGitRepo(projectRoot3)) return null;
21444
- return instrumentSync(join23(projectRoot3, ".hotsheet"), "git.getStatus", () => getGitStatusUnwrapped(projectRoot3, invoker));
21936
+ return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatus", () => getGitStatusUnwrapped(projectRoot3, invoker));
21445
21937
  }
21446
- function getGitStatusUnwrapped(projectRoot3, invoker) {
21938
+ async function getGitStatusUnwrapped(projectRoot3, invoker) {
21447
21939
  const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
21448
- const branchRes = invoker(["symbolic-ref", "--short", "HEAD"], root2);
21940
+ const branchRes = await invoker(["symbolic-ref", "--short", "HEAD"], root2);
21449
21941
  let branch;
21450
21942
  let detached = false;
21451
21943
  if (branchRes.status === 0 && branchRes.stdout.trim() !== "") {
21452
21944
  branch = branchRes.stdout.trim();
21453
21945
  } else {
21454
21946
  detached = true;
21455
- const sha = invoker(["rev-parse", "--short", "HEAD"], root2);
21947
+ const sha = await invoker(["rev-parse", "--short", "HEAD"], root2);
21456
21948
  branch = sha.status === 0 && sha.stdout.trim() !== "" ? sha.stdout.trim() : "(detached)";
21457
21949
  }
21458
- const porcelain = invoker(["status", "--porcelain=v1", "--no-renames"], root2);
21950
+ const porcelain = await invoker(["status", "--porcelain=v1", "--no-renames"], root2);
21459
21951
  const counts = porcelain.status === 0 ? bucketPorcelain(porcelain.stdout) : { staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
21460
21952
  let upstream = null;
21461
21953
  let ahead = 0;
21462
21954
  let behind = 0;
21463
21955
  if (!detached) {
21464
- const upRes = invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
21956
+ const upRes = await invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
21465
21957
  if (upRes.status === 0 && upRes.stdout.trim() !== "") {
21466
21958
  upstream = upRes.stdout.trim();
21467
- const aheadRes = invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
21959
+ const aheadRes = await invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
21468
21960
  if (aheadRes.status === 0) {
21469
21961
  const n = Number.parseInt(aheadRes.stdout.trim(), 10);
21470
21962
  if (Number.isFinite(n)) ahead = n;
21471
21963
  }
21472
- const behindRes = invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
21964
+ const behindRes = await invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
21473
21965
  if (behindRes.status === 0) {
21474
21966
  const n = Number.parseInt(behindRes.stdout.trim(), 10);
21475
21967
  if (Number.isFinite(n)) behind = n;
@@ -21492,16 +21984,16 @@ function getGitStatusUnwrapped(projectRoot3, invoker) {
21492
21984
  function getLastFetchedAt(projectRoot3) {
21493
21985
  return lastFetchedAt.get(projectRoot3) ?? null;
21494
21986
  }
21495
- function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
21987
+ async function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
21496
21988
  if (!isGitRepo(projectRoot3)) {
21497
21989
  return { ok: false, lastFetchedAt: null, error: "Not a git repository" };
21498
21990
  }
21499
21991
  const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
21500
- const upRes = invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
21992
+ const upRes = await invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
21501
21993
  if (upRes.status !== 0 || upRes.stdout.trim() === "") {
21502
21994
  return { ok: false, lastFetchedAt: null, error: "No upstream branch \u2014 set one with `git push -u <remote> <branch>`." };
21503
21995
  }
21504
- const fetchRes = invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
21996
+ const fetchRes = await invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
21505
21997
  if (fetchRes.status === 0) {
21506
21998
  const now = Date.now();
21507
21999
  lastFetchedAt.set(projectRoot3, now);
@@ -21528,11 +22020,11 @@ function bucketPorcelain(output) {
21528
22020
  }
21529
22021
  return out;
21530
22022
  }
21531
- function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
22023
+ async function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
21532
22024
  if (!isGitRepo(projectRoot3)) return null;
21533
- return instrumentSync(join23(projectRoot3, ".hotsheet"), "git.getStatusFiles", () => {
22025
+ return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatusFiles", async () => {
21534
22026
  const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
21535
- const res = invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
22027
+ const res = await invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
21536
22028
  if (res.status !== 0) return null;
21537
22029
  return bucketPorcelainFiles(res.stdout);
21538
22030
  });
@@ -21579,13 +22071,15 @@ function pushCapped(arr, item, onTruncate) {
21579
22071
  }
21580
22072
  arr.push(item);
21581
22073
  }
21582
- var SPAWN_TIMEOUT_MS, defaultInvoker, lastFetchedAt, FILES_PER_BUCKET_CAP;
22074
+ var execFileAsync2, SPAWN_TIMEOUT_MS, GIT_MAX_BUFFER, defaultInvoker, lastFetchedAt, FILES_PER_BUCKET_CAP;
21583
22075
  var init_status = __esm({
21584
22076
  "src/git/status.ts"() {
21585
22077
  "use strict";
21586
22078
  init_freezeLogger();
21587
22079
  init_gitignore();
22080
+ execFileAsync2 = promisify2(execFile3);
21588
22081
  SPAWN_TIMEOUT_MS = 2e3;
22082
+ GIT_MAX_BUFFER = 32 * 1024 * 1024;
21589
22083
  defaultInvoker = makeGitInvoker({ timeoutMs: SPAWN_TIMEOUT_MS });
21590
22084
  lastFetchedAt = /* @__PURE__ */ new Map();
21591
22085
  FILES_PER_BUCKET_CAP = 200;
@@ -21606,21 +22100,33 @@ __export(watcher_exports, {
21606
22100
  });
21607
22101
  import { existsSync as existsSync17, watch as fsWatch } from "fs";
21608
22102
  import { join as join24 } from "path";
21609
- function getCachedGitStatus(projectRoot3) {
22103
+ async function getCachedGitStatus(projectRoot3) {
21610
22104
  const entry = cache.get(projectRoot3);
21611
22105
  const now = Date.now();
21612
22106
  if (entry !== void 0 && now - entry.resolvedAt < CACHE_TTL_MS) {
21613
22107
  return entry.status;
21614
22108
  }
21615
- const status = getGitStatus(projectRoot3);
21616
- cache.set(projectRoot3, { status, resolvedAt: now });
21617
- return status;
22109
+ const pending2 = inFlight.get(projectRoot3);
22110
+ if (pending2 !== void 0) return pending2;
22111
+ const p = (async () => {
22112
+ try {
22113
+ const status = await getGitStatus(projectRoot3);
22114
+ cache.set(projectRoot3, { status, resolvedAt: Date.now() });
22115
+ return status;
22116
+ } finally {
22117
+ inFlight.delete(projectRoot3);
22118
+ }
22119
+ })();
22120
+ inFlight.set(projectRoot3, p);
22121
+ return p;
21618
22122
  }
21619
22123
  function _resetGitStatusCacheForTests() {
21620
22124
  cache.clear();
22125
+ inFlight.clear();
21621
22126
  }
21622
22127
  function dropGitStatusCache(projectRoot3) {
21623
22128
  cache.delete(projectRoot3);
22129
+ inFlight.delete(projectRoot3);
21624
22130
  }
21625
22131
  function subscribeToGitChanges(handler) {
21626
22132
  subscribers.add(handler);
@@ -21649,13 +22155,22 @@ function ensureGitWatcher(projectRoot3) {
21649
22155
  if (e2 === void 0) return;
21650
22156
  e2.debounce = null;
21651
22157
  cache.delete(projectRoot3);
22158
+ inFlight.delete(projectRoot3);
21652
22159
  e2.version++;
22160
+ if (!isProjectActive(join24(projectRoot3, ".hotsheet"))) return;
21653
22161
  for (const sub of subscribers) {
21654
22162
  try {
21655
22163
  sub(projectRoot3);
21656
22164
  } catch {
21657
22165
  }
21658
22166
  }
22167
+ void getBackgroundScheduler().submit({
22168
+ key: `git-refresh:${projectRoot3}`,
22169
+ priority: PRIORITY.GIT_STATUS,
22170
+ projectKey: projectRoot3,
22171
+ deferUnderLag: true,
22172
+ run: () => getCachedGitStatus(projectRoot3).then(() => void 0)
22173
+ });
21659
22174
  }, WATCHER_DEBOUNCE_MS);
21660
22175
  };
21661
22176
  for (const file2 of filenames) {
@@ -21682,19 +22197,23 @@ function disposeGitWatcher(projectRoot3) {
21682
22197
  watchers.delete(projectRoot3);
21683
22198
  }
21684
22199
  cache.delete(projectRoot3);
22200
+ inFlight.delete(projectRoot3);
21685
22201
  }
21686
22202
  function disposeAllGitWatchers() {
21687
22203
  for (const root2 of [...watchers.keys()]) disposeGitWatcher(root2);
21688
22204
  subscribers.clear();
21689
22205
  }
21690
- var CACHE_TTL_MS, cache, WATCHER_DEBOUNCE_MS, watchers, subscribers;
22206
+ var CACHE_TTL_MS, cache, inFlight, WATCHER_DEBOUNCE_MS, watchers, subscribers;
21691
22207
  var init_watcher = __esm({
21692
22208
  "src/git/watcher.ts"() {
21693
22209
  "use strict";
22210
+ init_activeProjects();
21694
22211
  init_gitignore();
22212
+ init_backgroundScheduler();
21695
22213
  init_status();
21696
22214
  CACHE_TTL_MS = 500;
21697
22215
  cache = /* @__PURE__ */ new Map();
22216
+ inFlight = /* @__PURE__ */ new Map();
21698
22217
  WATCHER_DEBOUNCE_MS = 250;
21699
22218
  watchers = /* @__PURE__ */ new Map();
21700
22219
  subscribers = /* @__PURE__ */ new Set();
@@ -23775,6 +24294,7 @@ async function runShutdownPipeline(reason) {
23775
24294
  await killShellCommands();
23776
24295
  await destroyTerminals();
23777
24296
  await disposeGitWatchers();
24297
+ await terminateHashWorkerStep();
23778
24298
  await snapshotDatabases();
23779
24299
  await closeDatabases();
23780
24300
  stopFreezeHeartbeat();
@@ -23807,6 +24327,14 @@ async function disposeGitWatchers() {
23807
24327
  console.error("[lifecycle] disposeAllGitWatchers error:", err);
23808
24328
  }
23809
24329
  }
24330
+ async function terminateHashWorkerStep() {
24331
+ try {
24332
+ const { terminateHashWorker: terminateHashWorker2 } = await Promise.resolve().then(() => (init_hashWorker(), hashWorker_exports));
24333
+ await terminateHashWorker2();
24334
+ } catch (err) {
24335
+ console.error("[lifecycle] terminateHashWorker error:", err);
24336
+ }
24337
+ }
23810
24338
  async function closeHttpServer() {
23811
24339
  if (httpServer === null) return;
23812
24340
  await new Promise((resolve11) => {
@@ -23901,25 +24429,25 @@ var init_mime_types = __esm({
23901
24429
  // src/open-in-file-manager.ts
23902
24430
  import { dirname as dirname6 } from "path";
23903
24431
  async function openInFileManager(dirPath) {
23904
- const { execFile: execFile7 } = await import("child_process");
24432
+ const { execFile: execFile8 } = await import("child_process");
23905
24433
  const platform2 = process.platform;
23906
24434
  if (platform2 === "darwin") {
23907
- execFile7("open", [dirPath]);
24435
+ execFile8("open", [dirPath]);
23908
24436
  } else if (platform2 === "win32") {
23909
- execFile7("explorer", [dirPath]);
24437
+ execFile8("explorer", [dirPath]);
23910
24438
  } else {
23911
- execFile7("xdg-open", [dirPath]);
24439
+ execFile8("xdg-open", [dirPath]);
23912
24440
  }
23913
24441
  }
23914
24442
  async function revealInFileManager(filePath) {
23915
- const { execFile: execFile7 } = await import("child_process");
24443
+ const { execFile: execFile8 } = await import("child_process");
23916
24444
  const platform2 = process.platform;
23917
24445
  if (platform2 === "darwin") {
23918
- execFile7("open", ["-R", filePath]);
24446
+ execFile8("open", ["-R", filePath]);
23919
24447
  } else if (platform2 === "win32") {
23920
- execFile7("explorer", ["/select,", filePath]);
24448
+ execFile8("explorer", ["/select,", filePath]);
23921
24449
  } else {
23922
- execFile7("xdg-open", [dirname6(filePath)]);
24450
+ execFile8("xdg-open", [dirname6(filePath)]);
23923
24451
  }
23924
24452
  }
23925
24453
  var init_open_in_file_manager = __esm({
@@ -24217,14 +24745,14 @@ __export(keychain_exports, {
24217
24745
  keychainGet: () => keychainGet,
24218
24746
  keychainSet: () => keychainSet
24219
24747
  });
24220
- import { execFile as execFile3 } from "child_process";
24748
+ import { execFile as execFile4 } from "child_process";
24221
24749
  import { platform } from "os";
24222
24750
  function makeService(pluginId) {
24223
24751
  return `${SERVICE_PREFIX}.${pluginId}`;
24224
24752
  }
24225
24753
  function exec(cmd, args) {
24226
24754
  return new Promise((resolve11) => {
24227
- execFile3(cmd, args, { timeout: 5e3 }, (error51, stdout) => {
24755
+ execFile4(cmd, args, { timeout: 5e3 }, (error51, stdout) => {
24228
24756
  resolve11({ stdout: stdout.trim(), exitCode: error51 ? error51.status ?? 1 : 0 });
24229
24757
  });
24230
24758
  });
@@ -24276,7 +24804,7 @@ async function linuxGet(service, account) {
24276
24804
  }
24277
24805
  async function linuxSet(service, account, password) {
24278
24806
  return new Promise((resolve11) => {
24279
- const proc = execFile3("secret-tool", [
24807
+ const proc = execFile4("secret-tool", [
24280
24808
  "store",
24281
24809
  "--label",
24282
24810
  `Hot Sheet: ${account}`,
@@ -26014,13 +26542,13 @@ var require_aspromise = __commonJS({
26014
26542
  "use strict";
26015
26543
  module.exports = asPromise;
26016
26544
  function asPromise(fn, ctx) {
26017
- var params = new Array(arguments.length - 1), offset = 0, index = 2, pending = true;
26545
+ var params = new Array(arguments.length - 1), offset = 0, index = 2, pending2 = true;
26018
26546
  while (index < arguments.length)
26019
26547
  params[offset++] = arguments[index++];
26020
26548
  return new Promise(function executor(resolve11, reject2) {
26021
26549
  params[offset] = function callback(err) {
26022
- if (pending) {
26023
- pending = false;
26550
+ if (pending2) {
26551
+ pending2 = false;
26024
26552
  if (err)
26025
26553
  reject2(err);
26026
26554
  else {
@@ -26034,8 +26562,8 @@ var require_aspromise = __commonJS({
26034
26562
  try {
26035
26563
  fn.apply(ctx || null, params);
26036
26564
  } catch (err) {
26037
- if (pending) {
26038
- pending = false;
26565
+ if (pending2) {
26566
+ pending2 = false;
26039
26567
  reject2(err);
26040
26568
  }
26041
26569
  }
@@ -32822,7 +33350,7 @@ __export(processPriority_exports, {
32822
33350
  bumpProcessPriorityBestEffort: () => bumpProcessPriorityBestEffort,
32823
33351
  shouldBumpProcessPriority: () => shouldBumpProcessPriority
32824
33352
  });
32825
- import { spawnSync as spawnSync2 } from "child_process";
33353
+ import { spawnSync } from "child_process";
32826
33354
  function shouldBumpProcessPriority(platform2) {
32827
33355
  return platform2 === "darwin";
32828
33356
  }
@@ -32834,7 +33362,7 @@ function bumpProcessPriorityBestEffort() {
32834
33362
  const args = buildTaskpolicyArgs(process.pid);
32835
33363
  let result;
32836
33364
  try {
32837
- result = spawnSync2("taskpolicy", args, { encoding: "utf8", timeout: 2e3 });
33365
+ result = spawnSync("taskpolicy", args, { encoding: "utf8", timeout: 2e3 });
32838
33366
  } catch (err) {
32839
33367
  console.warn(`[priority] taskpolicy spawn failed: ${err instanceof Error ? err.message : String(err)}`);
32840
33368
  return false;
@@ -32861,7 +33389,7 @@ var init_processPriority = __esm({
32861
33389
 
32862
33390
  // src/cli.ts
32863
33391
  init_backup();
32864
- import { execFile as execFile6 } from "child_process";
33392
+ import { execFile as execFile7 } from "child_process";
32865
33393
  import { existsSync as existsSync30, mkdirSync as mkdirSync18, realpathSync } from "fs";
32866
33394
  import { tmpdir as tmpdir3 } from "os";
32867
33395
  import { join as join36, resolve as resolve10 } from "path";
@@ -33365,7 +33893,7 @@ init_lifecycle2();
33365
33893
  init_mime_types();
33366
33894
  init_projects();
33367
33895
  import { serve } from "@hono/node-server";
33368
- import { execFile as execFile5 } from "child_process";
33896
+ import { execFile as execFile6 } from "child_process";
33369
33897
  import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
33370
33898
  import { Hono as Hono19 } from "hono";
33371
33899
  import { basename as basename5, dirname as dirname8, join as join34 } from "path";
@@ -33563,6 +34091,7 @@ function listAliveEntries(dataDir, isPidAlive3 = defaultIsPidAlive) {
33563
34091
  init_claude_hooks();
33564
34092
  init_commandLog();
33565
34093
  init_settings();
34094
+ init_ticketWorkIntervals();
33566
34095
  init_freezeLogger();
33567
34096
  init_file_settings();
33568
34097
  init_global_config();
@@ -33834,7 +34363,10 @@ channelRoutes.post("/channel/permission/dismiss", async (c) => {
33834
34363
  });
33835
34364
  channelRoutes.post("/channel/done", (_c) => {
33836
34365
  const secret = _c.get("projectSecret");
33837
- if (secret) channelDoneFlags.set(secret, true);
34366
+ if (secret) {
34367
+ channelDoneFlags.set(secret, true);
34368
+ void closeOpenTicketIntervalsForProject(secret);
34369
+ }
33838
34370
  addLogEntry("done", "incoming", "Claude finished", "").catch(() => {
33839
34371
  });
33840
34372
  notifyChange();
@@ -33932,6 +34464,7 @@ commandLogRoutes.get("/command-log/count", async (c) => {
33932
34464
  });
33933
34465
 
33934
34466
  // src/routes/dashboard.ts
34467
+ init_activeProjects();
33935
34468
  init_queries();
33936
34469
  init_stats();
33937
34470
  init_gitignore();
@@ -33947,6 +34480,7 @@ import { homedir as homedir6, tmpdir } from "os";
33947
34480
  import { join as join29, relative as relative2, resolve as resolve8 } from "path";
33948
34481
  var dashboardRoutes = new Hono5();
33949
34482
  dashboardRoutes.get("/poll", async (c) => {
34483
+ markProjectActive(c.get("dataDir"));
33950
34484
  const clientVersion = Math.max(0, parseInt(c.req.query("version") ?? "0", 10) || 0);
33951
34485
  const changeVersion2 = getChangeVersion();
33952
34486
  if (changeVersion2 > clientVersion) {
@@ -35014,12 +35548,12 @@ import { Hono as Hono13 } from "hono";
35014
35548
  // src/db/repair.ts
35015
35549
  init_backup();
35016
35550
  init_pglite();
35017
- import { execFile as execFile4 } from "child_process";
35551
+ import { execFile as execFile5 } from "child_process";
35018
35552
  import { cpSync as cpSync2, existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, rmSync as rmSync10, writeFileSync as writeFileSync18 } from "fs";
35019
35553
  import { tmpdir as tmpdir2 } from "os";
35020
35554
  import { join as join32 } from "path";
35021
- import { promisify as promisify2 } from "util";
35022
- var execFileP = promisify2(execFile4);
35555
+ import { promisify as promisify3 } from "util";
35556
+ var execFileP = promisify3(execFile5);
35023
35557
  async function findWorkingBackup(dataDir) {
35024
35558
  const backups = listBackups(dataDir);
35025
35559
  for (const backup of backups) {
@@ -35210,6 +35744,7 @@ dbRoutes.post("/repair/run-pg-resetwal", async (c) => {
35210
35744
  });
35211
35745
 
35212
35746
  // src/routes/git.ts
35747
+ init_activeProjects();
35213
35748
  import { Hono as Hono14 } from "hono";
35214
35749
  import { join as join33 } from "path";
35215
35750
 
@@ -35271,18 +35806,19 @@ init_watcher();
35271
35806
  init_gitignore();
35272
35807
  init_open_in_file_manager();
35273
35808
  var gitRoutes = new Hono14();
35274
- gitRoutes.get("/git/status", (c) => {
35809
+ gitRoutes.get("/git/status", async (c) => {
35275
35810
  const dataDir = c.get("dataDir");
35811
+ markProjectActive(dataDir);
35276
35812
  const projectRoot3 = projectRootFromDataDir(dataDir);
35277
35813
  const settings = readFileSettings(dataDir);
35278
35814
  if (settings.git_tracking_enabled === false) {
35279
35815
  return c.json(null);
35280
35816
  }
35281
35817
  ensureGitWatcher(projectRoot3);
35282
- const status = getCachedGitStatus(projectRoot3);
35818
+ const status = await getCachedGitStatus(projectRoot3);
35283
35819
  if (status === null) return c.json(null);
35284
35820
  if (c.req.query("files") === "true") {
35285
- const files = getGitStatusFiles(projectRoot3);
35821
+ const files = await getGitStatusFiles(projectRoot3);
35286
35822
  return c.json({ ...status, files });
35287
35823
  }
35288
35824
  return c.json(status);
@@ -35290,14 +35826,14 @@ gitRoutes.get("/git/status", (c) => {
35290
35826
  function projectRootFromDataDir(dataDir) {
35291
35827
  return dataDir.replace(/[\\/]\.hotsheet\/?$/, "");
35292
35828
  }
35293
- gitRoutes.post("/git/fetch", (c) => {
35829
+ gitRoutes.post("/git/fetch", async (c) => {
35294
35830
  const dataDir = c.get("dataDir");
35295
35831
  const projectRoot3 = projectRootFromDataDir(dataDir);
35296
35832
  const settings = readFileSettings(dataDir);
35297
35833
  if (settings.git_tracking_enabled === false) {
35298
35834
  return c.json({ ok: false, lastFetchedAt: null, error: "git tracking disabled in settings" });
35299
35835
  }
35300
- const result = runGitFetch(projectRoot3);
35836
+ const result = await runGitFetch(projectRoot3);
35301
35837
  if (result.ok) dropGitStatusCache(projectRoot3);
35302
35838
  return c.json(result);
35303
35839
  });
@@ -38246,54 +38782,62 @@ async function getPromptTimeline(promptId) {
38246
38782
  spans
38247
38783
  };
38248
38784
  }
38249
- async function getPerTicketRollup(ticketNumber) {
38785
+ async function getPerTicketRollup(ticketNumber, secret) {
38250
38786
  const db = await getTelemetryDb();
38251
38787
  const marker = `hotsheet:ticket=${ticketNumber}`;
38252
- const tagged = await db.query(
38253
- `SELECT DISTINCT prompt_id FROM otel_events
38254
- WHERE ${eventNameMatchSql("event_name", "user_prompt")}
38255
- AND prompt_id IS NOT NULL
38256
- AND body_json::text LIKE $1`,
38257
- [`%${marker}%`]
38258
- );
38259
- if (tagged.rows.length === 0) {
38260
- return { ticketNumber, promptCount: 0, totalCost: 0, totalTokens: 0, totalDurationSeconds: 0 };
38261
- }
38262
- const promptIds = tagged.rows.map((r) => r.prompt_id);
38263
- const sumsResult = await db.query(
38264
- `SELECT
38265
- SUM(COALESCE(
38266
- (attributes_json->>'cost')::numeric,
38267
- (attributes_json->>'cost_usd')::numeric,
38268
- 0
38269
- )) AS total_cost,
38270
- SUM(COALESCE(
38271
- (attributes_json->>'tokens')::numeric,
38272
- (attributes_json->>'total_tokens')::numeric,
38273
- (attributes_json->>'input_tokens')::numeric + (attributes_json->>'output_tokens')::numeric,
38274
- 0
38275
- )) AS total_tokens
38276
- FROM otel_events
38277
- WHERE ${eventNameMatchSql("event_name", "api_request")}
38278
- AND prompt_id = ANY($1::text[])`,
38279
- [promptIds]
38280
- );
38281
- const durationsResult = await db.query(
38282
- `SELECT SUM(EXTRACT(EPOCH FROM (max_ts - min_ts))) AS total_seconds
38283
- FROM (
38284
- SELECT MIN(ts) AS min_ts, MAX(ts) AS max_ts
38285
- FROM otel_events
38286
- WHERE prompt_id = ANY($1::text[])
38287
- GROUP BY prompt_id
38288
- ) AS per_prompt`,
38289
- [promptIds]
38788
+ const secretParam = secret !== void 0 && secret !== "" ? secret : null;
38789
+ const result = await db.query(
38790
+ `WITH marker_prompts AS (
38791
+ SELECT DISTINCT prompt_id FROM otel_events
38792
+ WHERE ${eventNameMatchSql("event_name", "user_prompt")}
38793
+ AND prompt_id IS NOT NULL
38794
+ AND body_json::text LIKE $1
38795
+ ),
38796
+ matched AS (
38797
+ SELECT
38798
+ e.prompt_id,
38799
+ e.ts,
38800
+ COALESCE(
38801
+ (e.attributes_json->>'cost')::numeric,
38802
+ (e.attributes_json->>'cost_usd')::numeric,
38803
+ 0
38804
+ ) AS cost,
38805
+ COALESCE(
38806
+ (e.attributes_json->>'tokens')::numeric,
38807
+ (e.attributes_json->>'total_tokens')::numeric,
38808
+ (e.attributes_json->>'input_tokens')::numeric + (e.attributes_json->>'output_tokens')::numeric,
38809
+ 0
38810
+ ) AS tokens
38811
+ FROM otel_events e
38812
+ WHERE ${eventNameMatchSql("e.event_name", "api_request")}
38813
+ AND (
38814
+ e.prompt_id IN (SELECT prompt_id FROM marker_prompts)
38815
+ OR (
38816
+ $2::text IS NOT NULL AND e.project_secret = $2 AND EXISTS (
38817
+ SELECT 1 FROM ticket_work_intervals i
38818
+ WHERE i.project_secret = $2 AND i.ticket_number = $3
38819
+ AND e.ts >= i.started_at AND e.ts <= COALESCE(i.ended_at, NOW())
38820
+ )
38821
+ )
38822
+ )
38823
+ )
38824
+ SELECT
38825
+ (SELECT COUNT(DISTINCT prompt_id) FROM matched) AS prompt_count,
38826
+ (SELECT COALESCE(SUM(cost), 0) FROM matched) AS total_cost,
38827
+ (SELECT COALESCE(SUM(tokens), 0) FROM matched) AS total_tokens,
38828
+ (SELECT COALESCE(SUM(dur), 0) FROM (
38829
+ SELECT EXTRACT(EPOCH FROM (MAX(ts) - MIN(ts))) AS dur
38830
+ FROM matched WHERE prompt_id IS NOT NULL GROUP BY prompt_id
38831
+ ) per_prompt) AS total_seconds`,
38832
+ [`%${marker}%`, secretParam, ticketNumber]
38290
38833
  );
38834
+ const row = result.rows[0];
38291
38835
  return {
38292
38836
  ticketNumber,
38293
- promptCount: promptIds.length,
38294
- totalCost: Number(sumsResult.rows[0]?.total_cost ?? 0),
38295
- totalTokens: Number(sumsResult.rows[0]?.total_tokens ?? 0),
38296
- totalDurationSeconds: Number(durationsResult.rows[0]?.total_seconds ?? 0)
38837
+ promptCount: Number(row.prompt_count ?? 0),
38838
+ totalCost: Number(row.total_cost ?? 0),
38839
+ totalTokens: Number(row.total_tokens ?? 0),
38840
+ totalDurationSeconds: Number(row.total_seconds ?? 0)
38297
38841
  };
38298
38842
  }
38299
38843
  async function getDashboardPayload(window2, timezone = "UTC", allowedSecrets = null, now = /* @__PURE__ */ new Date()) {
@@ -38375,7 +38919,8 @@ telemetryRoutes.get("/telemetry/prompt/:id", async (c) => {
38375
38919
  });
38376
38920
  telemetryRoutes.get("/telemetry/ticket/:number", async (c) => {
38377
38921
  const ticketNumber = c.req.param("number");
38378
- const rollup = await getPerTicketRollup(ticketNumber);
38922
+ const secret = c.get("projectSecret");
38923
+ const rollup = await getPerTicketRollup(ticketNumber, secret);
38379
38924
  return c.json(rollup);
38380
38925
  });
38381
38926
  telemetryRoutes.get("/telemetry/enabled-anywhere", (c) => {
@@ -38717,7 +39262,7 @@ async function startServer(port, dataDir, options) {
38717
39262
  `);
38718
39263
  if (options?.noOpen !== true) {
38719
39264
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
38720
- execFile5(openCmd, [url2]);
39265
+ execFile6(openCmd, [url2]);
38721
39266
  }
38722
39267
  return actualPort;
38723
39268
  }
@@ -38895,8 +39440,12 @@ async function startAndConfigure(port, dataDir, strictPort) {
38895
39440
  const secret = ensureSecret(dataDir, actualPort);
38896
39441
  const { bumpProcessPriorityBestEffort: bumpProcessPriorityBestEffort2 } = await Promise.resolve().then(() => (init_processPriority(), processPriority_exports));
38897
39442
  bumpProcessPriorityBestEffort2();
38898
- const { startServerEventLoopHeartbeat: startServerEventLoopHeartbeat2 } = await Promise.resolve().then(() => (init_freezeLogger(), freezeLogger_exports));
39443
+ const { startServerEventLoopHeartbeat: startServerEventLoopHeartbeat2, onServerWake: onServerWake2 } = await Promise.resolve().then(() => (init_freezeLogger(), freezeLogger_exports));
38899
39444
  startServerEventLoopHeartbeat2(dataDir);
39445
+ const { getBackgroundScheduler: getBackgroundScheduler2 } = await Promise.resolve().then(() => (init_backgroundScheduler(), backgroundScheduler_exports));
39446
+ onServerWake2(() => {
39447
+ getBackgroundScheduler2().noteWake();
39448
+ });
38900
39449
  initMarkdownSync(dataDir, actualPort);
38901
39450
  scheduleAllSync(dataDir);
38902
39451
  const { runWithDataDir: runWith } = await Promise.resolve().then(() => (init_connection(), connection_exports));
@@ -38945,7 +39494,7 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
38945
39494
  if (!noOpen) {
38946
39495
  const url2 = `http://localhost:${actualPort}`;
38947
39496
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
38948
- execFile6(openCmd, [url2]);
39497
+ execFile7(openCmd, [url2]);
38949
39498
  }
38950
39499
  }
38951
39500
  async function restorePreviousProjects(dataDir, actualPort) {