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.
- package/dist/cli.js +720 -171
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
15534
|
+
size = (await fsp.stat(p)).size;
|
|
15424
15535
|
} catch {
|
|
15425
15536
|
}
|
|
15426
15537
|
try {
|
|
15427
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16574
|
-
if (
|
|
16575
|
-
if (
|
|
16576
|
-
console.error(`[db] deferred recovery gave up after ${String(
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
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
|
-
|
|
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
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
21438
|
-
|
|
21439
|
-
|
|
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
|
|
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
|
|
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
|
|
21616
|
-
|
|
21617
|
-
|
|
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:
|
|
24432
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23905
24433
|
const platform2 = process.platform;
|
|
23906
24434
|
if (platform2 === "darwin") {
|
|
23907
|
-
|
|
24435
|
+
execFile8("open", [dirPath]);
|
|
23908
24436
|
} else if (platform2 === "win32") {
|
|
23909
|
-
|
|
24437
|
+
execFile8("explorer", [dirPath]);
|
|
23910
24438
|
} else {
|
|
23911
|
-
|
|
24439
|
+
execFile8("xdg-open", [dirPath]);
|
|
23912
24440
|
}
|
|
23913
24441
|
}
|
|
23914
24442
|
async function revealInFileManager(filePath) {
|
|
23915
|
-
const { execFile:
|
|
24443
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23916
24444
|
const platform2 = process.platform;
|
|
23917
24445
|
if (platform2 === "darwin") {
|
|
23918
|
-
|
|
24446
|
+
execFile8("open", ["-R", filePath]);
|
|
23919
24447
|
} else if (platform2 === "win32") {
|
|
23920
|
-
|
|
24448
|
+
execFile8("explorer", ["/select,", filePath]);
|
|
23921
24449
|
} else {
|
|
23922
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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 (
|
|
26023
|
-
|
|
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 (
|
|
26038
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
35022
|
-
var execFileP =
|
|
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
|
|
38253
|
-
|
|
38254
|
-
|
|
38255
|
-
|
|
38256
|
-
|
|
38257
|
-
|
|
38258
|
-
|
|
38259
|
-
|
|
38260
|
-
|
|
38261
|
-
|
|
38262
|
-
|
|
38263
|
-
|
|
38264
|
-
|
|
38265
|
-
|
|
38266
|
-
|
|
38267
|
-
|
|
38268
|
-
|
|
38269
|
-
|
|
38270
|
-
|
|
38271
|
-
|
|
38272
|
-
|
|
38273
|
-
|
|
38274
|
-
|
|
38275
|
-
|
|
38276
|
-
|
|
38277
|
-
|
|
38278
|
-
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38282
|
-
|
|
38283
|
-
|
|
38284
|
-
|
|
38285
|
-
|
|
38286
|
-
|
|
38287
|
-
|
|
38288
|
-
|
|
38289
|
-
|
|
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:
|
|
38294
|
-
totalCost: Number(
|
|
38295
|
-
totalTokens: Number(
|
|
38296
|
-
totalDurationSeconds: Number(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
39497
|
+
execFile7(openCmd, [url2]);
|
|
38949
39498
|
}
|
|
38950
39499
|
}
|
|
38951
39500
|
async function restorePreviousProjects(dataDir, actualPort) {
|