hotsheet 0.18.1-beta.1 → 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 +839 -187
- package/package.json +2 -2
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(),
|
|
@@ -15681,10 +15793,16 @@ __export(fsyncWrap_exports, {
|
|
|
15681
15793
|
fsyncDbDir: () => fsyncDbDir,
|
|
15682
15794
|
fsyncDbDirAsync: () => fsyncDbDirAsync,
|
|
15683
15795
|
fsyncDir: () => fsyncDir,
|
|
15684
|
-
fsyncDirAsync: () => fsyncDirAsync
|
|
15796
|
+
fsyncDirAsync: () => fsyncDirAsync,
|
|
15797
|
+
isUnsupportedFsyncError: () => isUnsupportedFsyncError
|
|
15685
15798
|
});
|
|
15686
15799
|
import { closeSync, existsSync as existsSync2, fsyncSync, openSync, promises as fsp2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
15687
15800
|
import { join as join2 } from "path";
|
|
15801
|
+
function isUnsupportedFsyncError(err, platform2 = process.platform) {
|
|
15802
|
+
if (platform2 !== "win32") return false;
|
|
15803
|
+
const code = err?.code;
|
|
15804
|
+
return code !== void 0 && WIN32_UNFLUSHABLE_CODES.has(code);
|
|
15805
|
+
}
|
|
15688
15806
|
function fsyncDir(path, fsyncFn = fsyncSync) {
|
|
15689
15807
|
if (!existsSync2(path)) return { filesFlushed: 0, errors: 0 };
|
|
15690
15808
|
const counters = { filesFlushed: 0, errors: 0 };
|
|
@@ -15719,8 +15837,10 @@ function walkAndFsync(dir, counters, fsyncFn) {
|
|
|
15719
15837
|
fsyncFn(fd);
|
|
15720
15838
|
counters.filesFlushed++;
|
|
15721
15839
|
} catch (err) {
|
|
15722
|
-
|
|
15723
|
-
|
|
15840
|
+
if (!isUnsupportedFsyncError(err)) {
|
|
15841
|
+
console.error(`[fsyncWrap] fsync ${p} failed:`, err);
|
|
15842
|
+
counters.errors++;
|
|
15843
|
+
}
|
|
15724
15844
|
} finally {
|
|
15725
15845
|
if (fd !== null) {
|
|
15726
15846
|
try {
|
|
@@ -15769,8 +15889,10 @@ async function walkAndFsyncAsync(dir, counters, fsyncFn) {
|
|
|
15769
15889
|
await fsyncFn(handle);
|
|
15770
15890
|
counters.filesFlushed++;
|
|
15771
15891
|
} catch (err) {
|
|
15772
|
-
|
|
15773
|
-
|
|
15892
|
+
if (!isUnsupportedFsyncError(err)) {
|
|
15893
|
+
console.error(`[fsyncWrap] fsync ${p} failed:`, err);
|
|
15894
|
+
counters.errors++;
|
|
15895
|
+
}
|
|
15774
15896
|
} finally {
|
|
15775
15897
|
if (handle !== null) {
|
|
15776
15898
|
try {
|
|
@@ -15785,10 +15907,11 @@ async function walkAndFsyncAsync(dir, counters, fsyncFn) {
|
|
|
15785
15907
|
async function fsyncDbDirAsync(dataDir, fsyncFn = defaultAsyncFsyncFn) {
|
|
15786
15908
|
return fsyncDirAsync(join2(dataDir, "db"), fsyncFn);
|
|
15787
15909
|
}
|
|
15788
|
-
var defaultAsyncFsyncFn;
|
|
15910
|
+
var WIN32_UNFLUSHABLE_CODES, defaultAsyncFsyncFn;
|
|
15789
15911
|
var init_fsyncWrap = __esm({
|
|
15790
15912
|
"src/db/fsyncWrap.ts"() {
|
|
15791
15913
|
"use strict";
|
|
15914
|
+
WIN32_UNFLUSHABLE_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "ENOTSUP", "EINVAL"]);
|
|
15792
15915
|
defaultAsyncFsyncFn = (handle) => handle.sync();
|
|
15793
15916
|
}
|
|
15794
15917
|
});
|
|
@@ -15938,10 +16061,14 @@ __export(freezeLogger_exports, {
|
|
|
15938
16061
|
FREEZE_LOG_MAX_BYTES: () => FREEZE_LOG_MAX_BYTES,
|
|
15939
16062
|
FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE: () => FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE,
|
|
15940
16063
|
LONG_TASK_THRESHOLD_MS: () => LONG_TASK_THRESHOLD_MS,
|
|
16064
|
+
WAKE_GAP_THRESHOLD_MS: () => WAKE_GAP_THRESHOLD_MS,
|
|
15941
16065
|
_resetForTesting: () => _resetForTesting,
|
|
16066
|
+
_simulateHeartbeatGapForTesting: () => _simulateHeartbeatGapForTesting,
|
|
15942
16067
|
appendFreezeLog: () => appendFreezeLog,
|
|
16068
|
+
getRecentEventLoopLagMs: () => getRecentEventLoopLagMs,
|
|
15943
16069
|
instrumentAsync: () => instrumentAsync,
|
|
15944
16070
|
instrumentSync: () => instrumentSync,
|
|
16071
|
+
onServerWake: () => onServerWake,
|
|
15945
16072
|
startServerEventLoopHeartbeat: () => startServerEventLoopHeartbeat,
|
|
15946
16073
|
stopServerEventLoopHeartbeat: () => stopServerEventLoopHeartbeat
|
|
15947
16074
|
});
|
|
@@ -16000,6 +16127,44 @@ async function rotateIfNeeded(path, pendingBytes) {
|
|
|
16000
16127
|
console.warn("[hotsheet freeze.log] rotate writeFile failed:", err instanceof Error ? err.message : String(err));
|
|
16001
16128
|
}
|
|
16002
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
|
+
}
|
|
16003
16168
|
function startServerEventLoopHeartbeat(dataDir) {
|
|
16004
16169
|
if (heartbeatTimer !== null) return;
|
|
16005
16170
|
heartbeatDataDir = dataDir;
|
|
@@ -16008,18 +16173,13 @@ function startServerEventLoopHeartbeat(dataDir) {
|
|
|
16008
16173
|
const now = process.hrtime.bigint();
|
|
16009
16174
|
const elapsedMs = Number(now - lastHeartbeatNs) / 1e6;
|
|
16010
16175
|
lastHeartbeatNs = now;
|
|
16011
|
-
|
|
16012
|
-
if (blockMs >= LONG_TASK_THRESHOLD_MS && heartbeatDataDir !== null) {
|
|
16013
|
-
void appendFreezeLog(heartbeatDataDir, {
|
|
16014
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16015
|
-
source: "server-heartbeat",
|
|
16016
|
-
durationMs: Math.round(blockMs),
|
|
16017
|
-
context: "event-loop blocked"
|
|
16018
|
-
});
|
|
16019
|
-
}
|
|
16176
|
+
handleHeartbeatGap(elapsedMs - HEARTBEAT_INTERVAL_MS);
|
|
16020
16177
|
}, HEARTBEAT_INTERVAL_MS);
|
|
16021
16178
|
heartbeatTimer.unref();
|
|
16022
16179
|
}
|
|
16180
|
+
function getRecentEventLoopLagMs() {
|
|
16181
|
+
return lastEventLoopLagMs;
|
|
16182
|
+
}
|
|
16023
16183
|
function stopServerEventLoopHeartbeat() {
|
|
16024
16184
|
if (heartbeatTimer !== null) {
|
|
16025
16185
|
clearInterval(heartbeatTimer);
|
|
@@ -16066,9 +16226,11 @@ function _resetForTesting() {
|
|
|
16066
16226
|
}
|
|
16067
16227
|
heartbeatDataDir = null;
|
|
16068
16228
|
lastHeartbeatNs = 0n;
|
|
16229
|
+
lastEventLoopLagMs = 0;
|
|
16230
|
+
wakeListeners.clear();
|
|
16069
16231
|
appendQueue.clear();
|
|
16070
16232
|
}
|
|
16071
|
-
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;
|
|
16072
16234
|
var init_freezeLogger = __esm({
|
|
16073
16235
|
"src/diagnostics/freezeLogger.ts"() {
|
|
16074
16236
|
"use strict";
|
|
@@ -16077,10 +16239,205 @@ var init_freezeLogger = __esm({
|
|
|
16077
16239
|
FREEZE_LOG_MAX_BYTES = 1048576;
|
|
16078
16240
|
FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE = 524288;
|
|
16079
16241
|
HEARTBEAT_INTERVAL_MS = 50;
|
|
16242
|
+
WAKE_GAP_THRESHOLD_MS = 1e4;
|
|
16080
16243
|
appendQueue = /* @__PURE__ */ new Map();
|
|
16081
16244
|
heartbeatTimer = null;
|
|
16082
16245
|
lastHeartbeatNs = 0n;
|
|
16246
|
+
lastEventLoopLagMs = 0;
|
|
16083
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;
|
|
16084
16441
|
}
|
|
16085
16442
|
});
|
|
16086
16443
|
|
|
@@ -16137,7 +16494,7 @@ function initSnapshotScheduler(dataDir) {
|
|
|
16137
16494
|
const state = getOrCreateState(dataDir);
|
|
16138
16495
|
if (state.safetyTimer !== null) return;
|
|
16139
16496
|
state.safetyTimer = setInterval(() => {
|
|
16140
|
-
if (state.dirty && !state.inProgress) void
|
|
16497
|
+
if (state.dirty && !state.inProgress) void submitSnapshotJob(dataDir);
|
|
16141
16498
|
}, numericSetting(dataDir, "db_snapshot_safety_interval_ms", DEFAULT_SAFETY_INTERVAL_MS));
|
|
16142
16499
|
state.safetyTimer.unref();
|
|
16143
16500
|
}
|
|
@@ -16150,9 +16507,20 @@ function scheduleSnapshot(dataDir) {
|
|
|
16150
16507
|
if (state.debounceTimer) clearTimeout(state.debounceTimer);
|
|
16151
16508
|
state.debounceTimer = setTimeout(() => {
|
|
16152
16509
|
state.debounceTimer = null;
|
|
16153
|
-
void
|
|
16510
|
+
void submitSnapshotJob(dir);
|
|
16154
16511
|
}, numericSetting(dir, "db_snapshot_debounce_ms", DEFAULT_DEBOUNCE_MS));
|
|
16155
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
|
+
}
|
|
16156
16524
|
async function writeSnapshotNow(dataDir) {
|
|
16157
16525
|
if (!isSnapshotProtectionEnabled(dataDir)) return null;
|
|
16158
16526
|
if (!existsSync4(join5(dataDir, "db"))) return null;
|
|
@@ -16194,7 +16562,7 @@ async function snapshotAllForShutdown() {
|
|
|
16194
16562
|
for (const dir of dirs) {
|
|
16195
16563
|
if (!isSnapshotProtectionEnabled(dir)) continue;
|
|
16196
16564
|
try {
|
|
16197
|
-
await
|
|
16565
|
+
await submitSnapshotJob(dir);
|
|
16198
16566
|
} catch (err) {
|
|
16199
16567
|
console.error(`[snapshot] shutdown snapshot failed for ${dir}:`, err);
|
|
16200
16568
|
}
|
|
@@ -16246,6 +16614,7 @@ var init_snapshot = __esm({
|
|
|
16246
16614
|
"use strict";
|
|
16247
16615
|
init_freezeLogger();
|
|
16248
16616
|
init_file_settings();
|
|
16617
|
+
init_backgroundScheduler();
|
|
16249
16618
|
init_connection();
|
|
16250
16619
|
DEFAULT_DEBOUNCE_MS = 2e3;
|
|
16251
16620
|
DEFAULT_SAFETY_INTERVAL_MS = 12e4;
|
|
@@ -16361,13 +16730,40 @@ function clearRecoveryMarker(dataDir) {
|
|
|
16361
16730
|
} catch {
|
|
16362
16731
|
}
|
|
16363
16732
|
}
|
|
16733
|
+
function pendingRecoveryPath(dataDir) {
|
|
16734
|
+
return join7(dataDir, PENDING_RECOVERY_FILENAME);
|
|
16735
|
+
}
|
|
16736
|
+
function readPendingRecovery(dataDir) {
|
|
16737
|
+
const path = pendingRecoveryPath(dataDir);
|
|
16738
|
+
if (!existsSync6(path)) return null;
|
|
16739
|
+
try {
|
|
16740
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
16741
|
+
const result = external_exports.object({ attempts: external_exports.number() }).loose().safeParse(parsed);
|
|
16742
|
+
return { attempts: result.success ? result.data.attempts : 1 };
|
|
16743
|
+
} catch {
|
|
16744
|
+
return { attempts: 1 };
|
|
16745
|
+
}
|
|
16746
|
+
}
|
|
16747
|
+
function writePendingRecovery(dataDir, attempts) {
|
|
16748
|
+
try {
|
|
16749
|
+
writeFileSync2(pendingRecoveryPath(dataDir), JSON.stringify({ attempts, requestedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
16750
|
+
} catch (writeErr) {
|
|
16751
|
+
console.error("Could not write pending-recovery marker:", getErrorMessage(writeErr));
|
|
16752
|
+
}
|
|
16753
|
+
}
|
|
16754
|
+
function clearPendingRecovery(dataDir) {
|
|
16755
|
+
try {
|
|
16756
|
+
rmSync2(pendingRecoveryPath(dataDir), { force: true });
|
|
16757
|
+
} catch {
|
|
16758
|
+
}
|
|
16759
|
+
}
|
|
16364
16760
|
function runWithDataDir(dataDir, fn) {
|
|
16365
16761
|
return requestDataDir.run(dataDir, fn);
|
|
16366
16762
|
}
|
|
16367
16763
|
function getDataDir() {
|
|
16368
16764
|
const contextDataDir = requestDataDir.getStore();
|
|
16369
16765
|
if (contextDataDir !== void 0) return contextDataDir;
|
|
16370
|
-
if (defaultDbPath !== null) return defaultDbPath.replace(
|
|
16766
|
+
if (defaultDbPath !== null) return defaultDbPath.replace(/[\\/]db$/, "");
|
|
16371
16767
|
throw new Error("Data directory not available. Call setDataDir() or use runWithDataDir().");
|
|
16372
16768
|
}
|
|
16373
16769
|
function setDataDir(dataDir) {
|
|
@@ -16439,6 +16835,8 @@ async function getDbForDir(dataDir) {
|
|
|
16439
16835
|
async function getDbByPath(dbPath) {
|
|
16440
16836
|
const existing = databases.get(dbPath);
|
|
16441
16837
|
if (existing) return existing;
|
|
16838
|
+
const recovered = await completeDeferredRecovery(dbPath);
|
|
16839
|
+
if (recovered !== null) return recovered;
|
|
16442
16840
|
let db;
|
|
16443
16841
|
try {
|
|
16444
16842
|
db = await openAndCacheDb(dbPath);
|
|
@@ -16461,8 +16859,16 @@ async function getDbByPath(dbPath) {
|
|
|
16461
16859
|
}
|
|
16462
16860
|
async function openAndCacheDb(dbPath, loadDataDir) {
|
|
16463
16861
|
const db = createPglite(dbPath, loadDataDir !== void 0 ? { loadDataDir } : {});
|
|
16464
|
-
|
|
16465
|
-
|
|
16862
|
+
try {
|
|
16863
|
+
await db.waitReady;
|
|
16864
|
+
await initSchema(db);
|
|
16865
|
+
} catch (err) {
|
|
16866
|
+
try {
|
|
16867
|
+
await db.close();
|
|
16868
|
+
} catch {
|
|
16869
|
+
}
|
|
16870
|
+
throw err;
|
|
16871
|
+
}
|
|
16466
16872
|
databases.set(dbPath, db);
|
|
16467
16873
|
return db;
|
|
16468
16874
|
}
|
|
@@ -16506,6 +16912,57 @@ async function tryRestoreFromSources(dbPath, dataDir) {
|
|
|
16506
16912
|
}
|
|
16507
16913
|
return null;
|
|
16508
16914
|
}
|
|
16915
|
+
async function renameDirWithRetry(from, to) {
|
|
16916
|
+
const maxAttempts = 5;
|
|
16917
|
+
for (let attempt = 1; ; attempt++) {
|
|
16918
|
+
try {
|
|
16919
|
+
renameSync(from, to);
|
|
16920
|
+
return;
|
|
16921
|
+
} catch (renameErr) {
|
|
16922
|
+
const code = renameErr.code;
|
|
16923
|
+
const retryable = code === "EPERM" || code === "EBUSY" || code === "EACCES" || code === "ENOTEMPTY";
|
|
16924
|
+
if (!retryable || attempt >= maxAttempts) throw renameErr;
|
|
16925
|
+
await new Promise((resolve11) => setTimeout(resolve11, 100 * attempt));
|
|
16926
|
+
}
|
|
16927
|
+
}
|
|
16928
|
+
}
|
|
16929
|
+
async function completeDeferredRecovery(dbPath) {
|
|
16930
|
+
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
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.`);
|
|
16935
|
+
clearPendingRecovery(dataDir);
|
|
16936
|
+
return null;
|
|
16937
|
+
}
|
|
16938
|
+
if (!existsSync6(dbPath)) {
|
|
16939
|
+
clearPendingRecovery(dataDir);
|
|
16940
|
+
return null;
|
|
16941
|
+
}
|
|
16942
|
+
console.error("[db] completing deferred recovery \u2014 a prior launch could not move the corrupt database aside in-process (Windows handle lock)\u2026");
|
|
16943
|
+
const corruptPath = `${dbPath}-corrupt-${Date.now()}`;
|
|
16944
|
+
try {
|
|
16945
|
+
await renameDirWithRetry(dbPath, corruptPath);
|
|
16946
|
+
} catch (renameErr) {
|
|
16947
|
+
writePendingRecovery(dataDir, pending2.attempts + 1);
|
|
16948
|
+
console.error(`[db] deferred recovery could not move db/ yet: ${getErrorMessage(renameErr)}`);
|
|
16949
|
+
return null;
|
|
16950
|
+
}
|
|
16951
|
+
const restored = await tryRestoreFromSources(dbPath, dataDir);
|
|
16952
|
+
writeRecoveryMarker(dataDir, {
|
|
16953
|
+
corruptPath,
|
|
16954
|
+
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16955
|
+
errorMessage: "Database was corrupt and could not be preserved in-process (Windows handle lock); recovered on the next restart.",
|
|
16956
|
+
...restored !== null ? { restoredFrom: restored.label, restoredTicketCount: restored.ticketCount } : {}
|
|
16957
|
+
});
|
|
16958
|
+
clearPendingRecovery(dataDir);
|
|
16959
|
+
if (restored !== null) {
|
|
16960
|
+
console.error(`[db] deferred recovery restored from ${restored.label} (${String(restored.ticketCount)} tickets).`);
|
|
16961
|
+
return restored.db;
|
|
16962
|
+
}
|
|
16963
|
+
console.error("[db] deferred recovery: no snapshot/backup could be loaded; starting with a fresh empty database.");
|
|
16964
|
+
return null;
|
|
16965
|
+
}
|
|
16509
16966
|
async function recoverFromOpenFailure(dbPath, err, forceRecover) {
|
|
16510
16967
|
const message = getErrorMessage(err);
|
|
16511
16968
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
@@ -16530,16 +16987,18 @@ async function recoverFromOpenFailure(dbPath, err, forceRecover) {
|
|
|
16530
16987
|
}
|
|
16531
16988
|
}
|
|
16532
16989
|
}
|
|
16990
|
+
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16533
16991
|
const corruptPath = `${dbPath}-corrupt-${Date.now()}`;
|
|
16534
16992
|
console.error(`Database appears to be corrupt. Preserving as ${corruptPath} ...`);
|
|
16535
16993
|
try {
|
|
16536
|
-
|
|
16994
|
+
await renameDirWithRetry(dbPath, corruptPath);
|
|
16537
16995
|
} catch (renameErr) {
|
|
16538
16996
|
const renameMessage = getErrorMessage(renameErr);
|
|
16539
|
-
|
|
16997
|
+
const prev = readPendingRecovery(dataDir);
|
|
16998
|
+
writePendingRecovery(dataDir, (prev?.attempts ?? 0) + 1);
|
|
16999
|
+
console.error(`Could not preserve corrupt database directory in-process: ${renameMessage}. Wrote a pending-recovery marker \u2014 Hot Sheet will auto-recover from the latest snapshot on the next restart.`);
|
|
16540
17000
|
throw err;
|
|
16541
17001
|
}
|
|
16542
|
-
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16543
17002
|
const restored = await tryRestoreFromSources(dbPath, dataDir);
|
|
16544
17003
|
if (restored !== null) {
|
|
16545
17004
|
writeRecoveryMarker(dataDir, {
|
|
@@ -16782,6 +17241,22 @@ async function initSchema(db) {
|
|
|
16782
17241
|
CREATE INDEX IF NOT EXISTS idx_otel_spans_session_ts ON otel_spans(session_id, start_ts);
|
|
16783
17242
|
CREATE INDEX IF NOT EXISTS idx_otel_spans_prompt ON otel_spans(prompt_id);
|
|
16784
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);
|
|
16785
17260
|
`);
|
|
16786
17261
|
await migrateNoteIds(db);
|
|
16787
17262
|
}
|
|
@@ -16808,15 +17283,17 @@ async function migrateNoteIds(db) {
|
|
|
16808
17283
|
}
|
|
16809
17284
|
}
|
|
16810
17285
|
}
|
|
16811
|
-
var SCHEMA_VERSION, RECOVERY_MARKER_FILENAME, databases, defaultDbPath, requestDataDir;
|
|
17286
|
+
var SCHEMA_VERSION, RECOVERY_MARKER_FILENAME, PENDING_RECOVERY_FILENAME, MAX_DEFERRED_RECOVERY_ATTEMPTS, databases, defaultDbPath, requestDataDir;
|
|
16812
17287
|
var init_connection = __esm({
|
|
16813
17288
|
"src/db/connection.ts"() {
|
|
16814
17289
|
"use strict";
|
|
16815
17290
|
init_zod();
|
|
16816
17291
|
init_errorMessage();
|
|
16817
17292
|
init_pglite();
|
|
16818
|
-
SCHEMA_VERSION =
|
|
17293
|
+
SCHEMA_VERSION = 5;
|
|
16819
17294
|
RECOVERY_MARKER_FILENAME = ".db-recovery-marker.json";
|
|
17295
|
+
PENDING_RECOVERY_FILENAME = ".db-pending-recovery.json";
|
|
17296
|
+
MAX_DEFERRED_RECOVERY_ATTEMPTS = 3;
|
|
16820
17297
|
databases = /* @__PURE__ */ new Map();
|
|
16821
17298
|
defaultDbPath = null;
|
|
16822
17299
|
requestDataDir = new AsyncLocalStorage();
|
|
@@ -16908,20 +17385,27 @@ function getOrCreateState2(dataDir) {
|
|
|
16908
17385
|
}
|
|
16909
17386
|
return state;
|
|
16910
17387
|
}
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
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
|
+
}
|
|
16916
17406
|
}
|
|
16917
|
-
}
|
|
16918
|
-
|
|
16919
|
-
activeBackup = p;
|
|
16920
|
-
try {
|
|
16921
|
-
return await p;
|
|
16922
|
-
} finally {
|
|
16923
|
-
if (activeBackup === p) activeBackup = null;
|
|
16924
|
-
}
|
|
17407
|
+
});
|
|
17408
|
+
return out;
|
|
16925
17409
|
}
|
|
16926
17410
|
function backupsDir(dataDir) {
|
|
16927
17411
|
return getBackupDir(dataDir);
|
|
@@ -17185,12 +17669,18 @@ function initBackupScheduler(dataDir) {
|
|
|
17185
17669
|
});
|
|
17186
17670
|
}, 3e4);
|
|
17187
17671
|
state.attachmentGcInterval = setInterval(() => {
|
|
17188
|
-
void
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17192
|
-
|
|
17193
|
-
|
|
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
|
+
})
|
|
17194
17684
|
});
|
|
17195
17685
|
}, 24 * 60 * 60 * 1e3);
|
|
17196
17686
|
}
|
|
@@ -17209,7 +17699,7 @@ async function triggerMissedBackups(dataDir) {
|
|
|
17209
17699
|
await createBackup(dataDir, tier);
|
|
17210
17700
|
}
|
|
17211
17701
|
}
|
|
17212
|
-
var TIERS, backupStates, activePreviews,
|
|
17702
|
+
var TIERS, backupStates, activePreviews, backupLockSeq, BACKUP_EXCLUSIVE_GROUP, VALID_TIERS;
|
|
17213
17703
|
var init_backup = __esm({
|
|
17214
17704
|
"src/backup.ts"() {
|
|
17215
17705
|
"use strict";
|
|
@@ -17220,6 +17710,7 @@ var init_backup = __esm({
|
|
|
17220
17710
|
init_dbJsonExport();
|
|
17221
17711
|
init_freezeLogger();
|
|
17222
17712
|
init_file_settings();
|
|
17713
|
+
init_backgroundScheduler();
|
|
17223
17714
|
TIERS = {
|
|
17224
17715
|
"5min": { intervalMs: 5 * 60 * 1e3, maxAge: 60 * 60 * 1e3, maxCount: 12 },
|
|
17225
17716
|
"hourly": { intervalMs: 60 * 60 * 1e3, maxAge: 12 * 60 * 60 * 1e3, maxCount: 12 },
|
|
@@ -17227,7 +17718,8 @@ var init_backup = __esm({
|
|
|
17227
17718
|
};
|
|
17228
17719
|
backupStates = /* @__PURE__ */ new Map();
|
|
17229
17720
|
activePreviews = /* @__PURE__ */ new Map();
|
|
17230
|
-
|
|
17721
|
+
backupLockSeq = 0;
|
|
17722
|
+
BACKUP_EXCLUSIVE_GROUP = "backup";
|
|
17231
17723
|
VALID_TIERS = /* @__PURE__ */ new Set(["5min", "hourly", "daily"]);
|
|
17232
17724
|
}
|
|
17233
17725
|
});
|
|
@@ -17802,6 +18294,47 @@ var init_ticketNumber = __esm({
|
|
|
17802
18294
|
}
|
|
17803
18295
|
});
|
|
17804
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
|
+
|
|
17805
18338
|
// src/db/tickets.ts
|
|
17806
18339
|
function escapeIlike(value) {
|
|
17807
18340
|
return value.replace(/[%_\\]/g, "\\$&");
|
|
@@ -17934,7 +18467,17 @@ async function updateTicket(id, updates, options) {
|
|
|
17934
18467
|
`UPDATE tickets SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
17935
18468
|
values
|
|
17936
18469
|
);
|
|
17937
|
-
|
|
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;
|
|
17938
18481
|
}
|
|
17939
18482
|
async function deleteTicket(id) {
|
|
17940
18483
|
await updateTicket(id, { status: "deleted" });
|
|
@@ -18273,9 +18816,11 @@ var QUERYABLE_FIELDS, PRIORITY_ORD, STATUS_ORD, PRIORITY_RANK, STATUS_RANK;
|
|
|
18273
18816
|
var init_tickets = __esm({
|
|
18274
18817
|
"src/db/tickets.ts"() {
|
|
18275
18818
|
"use strict";
|
|
18819
|
+
init_file_settings();
|
|
18276
18820
|
init_ticketNumber();
|
|
18277
18821
|
init_connection();
|
|
18278
18822
|
init_notes();
|
|
18823
|
+
init_ticketWorkIntervals();
|
|
18279
18824
|
QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["category", "priority", "status", "title", "details", "up_next", "tags"]);
|
|
18280
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`;
|
|
18281
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`;
|
|
@@ -19380,7 +19925,14 @@ function scheduleWorklistSync(dir) {
|
|
|
19380
19925
|
if (!state) return;
|
|
19381
19926
|
if (state.worklistTimeout) clearTimeout(state.worklistTimeout);
|
|
19382
19927
|
state.worklistTimeout = setTimeout(() => {
|
|
19383
|
-
|
|
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
|
+
});
|
|
19384
19936
|
}, WORKLIST_SYNC_DEBOUNCE_MS);
|
|
19385
19937
|
}
|
|
19386
19938
|
function scheduleOpenTicketsSync(dir) {
|
|
@@ -19388,7 +19940,14 @@ function scheduleOpenTicketsSync(dir) {
|
|
|
19388
19940
|
if (!state) return;
|
|
19389
19941
|
if (state.openTicketsTimeout) clearTimeout(state.openTicketsTimeout);
|
|
19390
19942
|
state.openTicketsTimeout = setTimeout(() => {
|
|
19391
|
-
|
|
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
|
+
});
|
|
19392
19951
|
}, OPEN_TICKETS_SYNC_DEBOUNCE_MS);
|
|
19393
19952
|
}
|
|
19394
19953
|
function scheduleAllSync(dir) {
|
|
@@ -19706,6 +20265,7 @@ var init_markdown = __esm({
|
|
|
19706
20265
|
init_freezeLogger();
|
|
19707
20266
|
init_file_settings();
|
|
19708
20267
|
init_limits();
|
|
20268
|
+
init_backgroundScheduler();
|
|
19709
20269
|
init_schemas3();
|
|
19710
20270
|
syncStates = /* @__PURE__ */ new Map();
|
|
19711
20271
|
defaultDataDir2 = null;
|
|
@@ -20394,7 +20954,7 @@ function claudeAllowRulePattern(dataDir) {
|
|
|
20394
20954
|
return `mcp__${getMcpServerKey(dataDir)}__*`;
|
|
20395
20955
|
}
|
|
20396
20956
|
function projectRoot(dataDir) {
|
|
20397
|
-
return dataDir.replace(
|
|
20957
|
+
return dataDir.replace(/[\\/]\.hotsheet[\\/]?$/, "");
|
|
20398
20958
|
}
|
|
20399
20959
|
function claudeDir(dataDir) {
|
|
20400
20960
|
return join18(projectRoot(dataDir), ".claude");
|
|
@@ -20514,7 +21074,7 @@ function getChannelServerPath() {
|
|
|
20514
21074
|
return { command: process.execPath, args: [distPath] };
|
|
20515
21075
|
}
|
|
20516
21076
|
function projectRoot2(dataDir) {
|
|
20517
|
-
return dataDir.replace(
|
|
21077
|
+
return dataDir.replace(/[\\/]\.hotsheet[\\/]?$/, "");
|
|
20518
21078
|
}
|
|
20519
21079
|
function registerChannel(dataDir) {
|
|
20520
21080
|
const root2 = projectRoot2(dataDir);
|
|
@@ -21262,6 +21822,25 @@ var init_sessionStore = __esm({
|
|
|
21262
21822
|
}
|
|
21263
21823
|
});
|
|
21264
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
|
+
|
|
21265
21844
|
// src/gitignore.ts
|
|
21266
21845
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
21267
21846
|
import { appendFileSync as appendFileSync3, existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
|
|
@@ -21317,56 +21896,72 @@ var init_gitignore = __esm({
|
|
|
21317
21896
|
});
|
|
21318
21897
|
|
|
21319
21898
|
// src/git/status.ts
|
|
21320
|
-
import {
|
|
21899
|
+
import { execFile as execFile3 } from "child_process";
|
|
21321
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
|
+
}
|
|
21322
21907
|
function makeGitInvoker({ timeoutMs, includeStderr = false }) {
|
|
21323
|
-
return (args, cwd) => {
|
|
21324
|
-
const
|
|
21908
|
+
return async (args, cwd) => {
|
|
21909
|
+
const opts = {
|
|
21325
21910
|
cwd,
|
|
21326
21911
|
encoding: "utf-8",
|
|
21327
21912
|
timeout: timeoutMs,
|
|
21913
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
21328
21914
|
env: {
|
|
21329
21915
|
...process.env,
|
|
21330
21916
|
GIT_TERMINAL_PROMPT: "0",
|
|
21331
21917
|
GIT_OPTIONAL_LOCKS: "0"
|
|
21332
21918
|
}
|
|
21333
|
-
}
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
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
|
+
}
|
|
21337
21932
|
};
|
|
21338
21933
|
}
|
|
21339
|
-
function getGitStatus(projectRoot3, invoker = defaultInvoker) {
|
|
21934
|
+
async function getGitStatus(projectRoot3, invoker = defaultInvoker) {
|
|
21340
21935
|
if (!isGitRepo(projectRoot3)) return null;
|
|
21341
|
-
return
|
|
21936
|
+
return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatus", () => getGitStatusUnwrapped(projectRoot3, invoker));
|
|
21342
21937
|
}
|
|
21343
|
-
function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
21938
|
+
async function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
21344
21939
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21345
|
-
const branchRes = invoker(["symbolic-ref", "--short", "HEAD"], root2);
|
|
21940
|
+
const branchRes = await invoker(["symbolic-ref", "--short", "HEAD"], root2);
|
|
21346
21941
|
let branch;
|
|
21347
21942
|
let detached = false;
|
|
21348
21943
|
if (branchRes.status === 0 && branchRes.stdout.trim() !== "") {
|
|
21349
21944
|
branch = branchRes.stdout.trim();
|
|
21350
21945
|
} else {
|
|
21351
21946
|
detached = true;
|
|
21352
|
-
const sha = invoker(["rev-parse", "--short", "HEAD"], root2);
|
|
21947
|
+
const sha = await invoker(["rev-parse", "--short", "HEAD"], root2);
|
|
21353
21948
|
branch = sha.status === 0 && sha.stdout.trim() !== "" ? sha.stdout.trim() : "(detached)";
|
|
21354
21949
|
}
|
|
21355
|
-
const porcelain = invoker(["status", "--porcelain=v1", "--no-renames"], root2);
|
|
21950
|
+
const porcelain = await invoker(["status", "--porcelain=v1", "--no-renames"], root2);
|
|
21356
21951
|
const counts = porcelain.status === 0 ? bucketPorcelain(porcelain.stdout) : { staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
|
|
21357
21952
|
let upstream = null;
|
|
21358
21953
|
let ahead = 0;
|
|
21359
21954
|
let behind = 0;
|
|
21360
21955
|
if (!detached) {
|
|
21361
|
-
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);
|
|
21362
21957
|
if (upRes.status === 0 && upRes.stdout.trim() !== "") {
|
|
21363
21958
|
upstream = upRes.stdout.trim();
|
|
21364
|
-
const aheadRes = invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
|
|
21959
|
+
const aheadRes = await invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
|
|
21365
21960
|
if (aheadRes.status === 0) {
|
|
21366
21961
|
const n = Number.parseInt(aheadRes.stdout.trim(), 10);
|
|
21367
21962
|
if (Number.isFinite(n)) ahead = n;
|
|
21368
21963
|
}
|
|
21369
|
-
const behindRes = invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
|
|
21964
|
+
const behindRes = await invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
|
|
21370
21965
|
if (behindRes.status === 0) {
|
|
21371
21966
|
const n = Number.parseInt(behindRes.stdout.trim(), 10);
|
|
21372
21967
|
if (Number.isFinite(n)) behind = n;
|
|
@@ -21389,16 +21984,16 @@ function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
|
21389
21984
|
function getLastFetchedAt(projectRoot3) {
|
|
21390
21985
|
return lastFetchedAt.get(projectRoot3) ?? null;
|
|
21391
21986
|
}
|
|
21392
|
-
function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
|
|
21987
|
+
async function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
|
|
21393
21988
|
if (!isGitRepo(projectRoot3)) {
|
|
21394
21989
|
return { ok: false, lastFetchedAt: null, error: "Not a git repository" };
|
|
21395
21990
|
}
|
|
21396
21991
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21397
|
-
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);
|
|
21398
21993
|
if (upRes.status !== 0 || upRes.stdout.trim() === "") {
|
|
21399
21994
|
return { ok: false, lastFetchedAt: null, error: "No upstream branch \u2014 set one with `git push -u <remote> <branch>`." };
|
|
21400
21995
|
}
|
|
21401
|
-
const fetchRes = invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
|
|
21996
|
+
const fetchRes = await invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
|
|
21402
21997
|
if (fetchRes.status === 0) {
|
|
21403
21998
|
const now = Date.now();
|
|
21404
21999
|
lastFetchedAt.set(projectRoot3, now);
|
|
@@ -21425,11 +22020,11 @@ function bucketPorcelain(output) {
|
|
|
21425
22020
|
}
|
|
21426
22021
|
return out;
|
|
21427
22022
|
}
|
|
21428
|
-
function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
|
|
22023
|
+
async function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
|
|
21429
22024
|
if (!isGitRepo(projectRoot3)) return null;
|
|
21430
|
-
return
|
|
22025
|
+
return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatusFiles", async () => {
|
|
21431
22026
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21432
|
-
const res = invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
|
|
22027
|
+
const res = await invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
|
|
21433
22028
|
if (res.status !== 0) return null;
|
|
21434
22029
|
return bucketPorcelainFiles(res.stdout);
|
|
21435
22030
|
});
|
|
@@ -21476,13 +22071,15 @@ function pushCapped(arr, item, onTruncate) {
|
|
|
21476
22071
|
}
|
|
21477
22072
|
arr.push(item);
|
|
21478
22073
|
}
|
|
21479
|
-
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;
|
|
21480
22075
|
var init_status = __esm({
|
|
21481
22076
|
"src/git/status.ts"() {
|
|
21482
22077
|
"use strict";
|
|
21483
22078
|
init_freezeLogger();
|
|
21484
22079
|
init_gitignore();
|
|
22080
|
+
execFileAsync2 = promisify2(execFile3);
|
|
21485
22081
|
SPAWN_TIMEOUT_MS = 2e3;
|
|
22082
|
+
GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
21486
22083
|
defaultInvoker = makeGitInvoker({ timeoutMs: SPAWN_TIMEOUT_MS });
|
|
21487
22084
|
lastFetchedAt = /* @__PURE__ */ new Map();
|
|
21488
22085
|
FILES_PER_BUCKET_CAP = 200;
|
|
@@ -21503,21 +22100,33 @@ __export(watcher_exports, {
|
|
|
21503
22100
|
});
|
|
21504
22101
|
import { existsSync as existsSync17, watch as fsWatch } from "fs";
|
|
21505
22102
|
import { join as join24 } from "path";
|
|
21506
|
-
function getCachedGitStatus(projectRoot3) {
|
|
22103
|
+
async function getCachedGitStatus(projectRoot3) {
|
|
21507
22104
|
const entry = cache.get(projectRoot3);
|
|
21508
22105
|
const now = Date.now();
|
|
21509
22106
|
if (entry !== void 0 && now - entry.resolvedAt < CACHE_TTL_MS) {
|
|
21510
22107
|
return entry.status;
|
|
21511
22108
|
}
|
|
21512
|
-
const
|
|
21513
|
-
|
|
21514
|
-
|
|
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;
|
|
21515
22122
|
}
|
|
21516
22123
|
function _resetGitStatusCacheForTests() {
|
|
21517
22124
|
cache.clear();
|
|
22125
|
+
inFlight.clear();
|
|
21518
22126
|
}
|
|
21519
22127
|
function dropGitStatusCache(projectRoot3) {
|
|
21520
22128
|
cache.delete(projectRoot3);
|
|
22129
|
+
inFlight.delete(projectRoot3);
|
|
21521
22130
|
}
|
|
21522
22131
|
function subscribeToGitChanges(handler) {
|
|
21523
22132
|
subscribers.add(handler);
|
|
@@ -21546,13 +22155,22 @@ function ensureGitWatcher(projectRoot3) {
|
|
|
21546
22155
|
if (e2 === void 0) return;
|
|
21547
22156
|
e2.debounce = null;
|
|
21548
22157
|
cache.delete(projectRoot3);
|
|
22158
|
+
inFlight.delete(projectRoot3);
|
|
21549
22159
|
e2.version++;
|
|
22160
|
+
if (!isProjectActive(join24(projectRoot3, ".hotsheet"))) return;
|
|
21550
22161
|
for (const sub of subscribers) {
|
|
21551
22162
|
try {
|
|
21552
22163
|
sub(projectRoot3);
|
|
21553
22164
|
} catch {
|
|
21554
22165
|
}
|
|
21555
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
|
+
});
|
|
21556
22174
|
}, WATCHER_DEBOUNCE_MS);
|
|
21557
22175
|
};
|
|
21558
22176
|
for (const file2 of filenames) {
|
|
@@ -21579,19 +22197,23 @@ function disposeGitWatcher(projectRoot3) {
|
|
|
21579
22197
|
watchers.delete(projectRoot3);
|
|
21580
22198
|
}
|
|
21581
22199
|
cache.delete(projectRoot3);
|
|
22200
|
+
inFlight.delete(projectRoot3);
|
|
21582
22201
|
}
|
|
21583
22202
|
function disposeAllGitWatchers() {
|
|
21584
22203
|
for (const root2 of [...watchers.keys()]) disposeGitWatcher(root2);
|
|
21585
22204
|
subscribers.clear();
|
|
21586
22205
|
}
|
|
21587
|
-
var CACHE_TTL_MS, cache, WATCHER_DEBOUNCE_MS, watchers, subscribers;
|
|
22206
|
+
var CACHE_TTL_MS, cache, inFlight, WATCHER_DEBOUNCE_MS, watchers, subscribers;
|
|
21588
22207
|
var init_watcher = __esm({
|
|
21589
22208
|
"src/git/watcher.ts"() {
|
|
21590
22209
|
"use strict";
|
|
22210
|
+
init_activeProjects();
|
|
21591
22211
|
init_gitignore();
|
|
22212
|
+
init_backgroundScheduler();
|
|
21592
22213
|
init_status();
|
|
21593
22214
|
CACHE_TTL_MS = 500;
|
|
21594
22215
|
cache = /* @__PURE__ */ new Map();
|
|
22216
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
21595
22217
|
WATCHER_DEBOUNCE_MS = 250;
|
|
21596
22218
|
watchers = /* @__PURE__ */ new Map();
|
|
21597
22219
|
subscribers = /* @__PURE__ */ new Set();
|
|
@@ -23672,6 +24294,7 @@ async function runShutdownPipeline(reason) {
|
|
|
23672
24294
|
await killShellCommands();
|
|
23673
24295
|
await destroyTerminals();
|
|
23674
24296
|
await disposeGitWatchers();
|
|
24297
|
+
await terminateHashWorkerStep();
|
|
23675
24298
|
await snapshotDatabases();
|
|
23676
24299
|
await closeDatabases();
|
|
23677
24300
|
stopFreezeHeartbeat();
|
|
@@ -23704,6 +24327,14 @@ async function disposeGitWatchers() {
|
|
|
23704
24327
|
console.error("[lifecycle] disposeAllGitWatchers error:", err);
|
|
23705
24328
|
}
|
|
23706
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
|
+
}
|
|
23707
24338
|
async function closeHttpServer() {
|
|
23708
24339
|
if (httpServer === null) return;
|
|
23709
24340
|
await new Promise((resolve11) => {
|
|
@@ -23798,25 +24429,25 @@ var init_mime_types = __esm({
|
|
|
23798
24429
|
// src/open-in-file-manager.ts
|
|
23799
24430
|
import { dirname as dirname6 } from "path";
|
|
23800
24431
|
async function openInFileManager(dirPath) {
|
|
23801
|
-
const { execFile:
|
|
24432
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23802
24433
|
const platform2 = process.platform;
|
|
23803
24434
|
if (platform2 === "darwin") {
|
|
23804
|
-
|
|
24435
|
+
execFile8("open", [dirPath]);
|
|
23805
24436
|
} else if (platform2 === "win32") {
|
|
23806
|
-
|
|
24437
|
+
execFile8("explorer", [dirPath]);
|
|
23807
24438
|
} else {
|
|
23808
|
-
|
|
24439
|
+
execFile8("xdg-open", [dirPath]);
|
|
23809
24440
|
}
|
|
23810
24441
|
}
|
|
23811
24442
|
async function revealInFileManager(filePath) {
|
|
23812
|
-
const { execFile:
|
|
24443
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23813
24444
|
const platform2 = process.platform;
|
|
23814
24445
|
if (platform2 === "darwin") {
|
|
23815
|
-
|
|
24446
|
+
execFile8("open", ["-R", filePath]);
|
|
23816
24447
|
} else if (platform2 === "win32") {
|
|
23817
|
-
|
|
24448
|
+
execFile8("explorer", ["/select,", filePath]);
|
|
23818
24449
|
} else {
|
|
23819
|
-
|
|
24450
|
+
execFile8("xdg-open", [dirname6(filePath)]);
|
|
23820
24451
|
}
|
|
23821
24452
|
}
|
|
23822
24453
|
var init_open_in_file_manager = __esm({
|
|
@@ -24114,14 +24745,14 @@ __export(keychain_exports, {
|
|
|
24114
24745
|
keychainGet: () => keychainGet,
|
|
24115
24746
|
keychainSet: () => keychainSet
|
|
24116
24747
|
});
|
|
24117
|
-
import { execFile as
|
|
24748
|
+
import { execFile as execFile4 } from "child_process";
|
|
24118
24749
|
import { platform } from "os";
|
|
24119
24750
|
function makeService(pluginId) {
|
|
24120
24751
|
return `${SERVICE_PREFIX}.${pluginId}`;
|
|
24121
24752
|
}
|
|
24122
24753
|
function exec(cmd, args) {
|
|
24123
24754
|
return new Promise((resolve11) => {
|
|
24124
|
-
|
|
24755
|
+
execFile4(cmd, args, { timeout: 5e3 }, (error51, stdout) => {
|
|
24125
24756
|
resolve11({ stdout: stdout.trim(), exitCode: error51 ? error51.status ?? 1 : 0 });
|
|
24126
24757
|
});
|
|
24127
24758
|
});
|
|
@@ -24173,7 +24804,7 @@ async function linuxGet(service, account) {
|
|
|
24173
24804
|
}
|
|
24174
24805
|
async function linuxSet(service, account, password) {
|
|
24175
24806
|
return new Promise((resolve11) => {
|
|
24176
|
-
const proc =
|
|
24807
|
+
const proc = execFile4("secret-tool", [
|
|
24177
24808
|
"store",
|
|
24178
24809
|
"--label",
|
|
24179
24810
|
`Hot Sheet: ${account}`,
|
|
@@ -25911,13 +26542,13 @@ var require_aspromise = __commonJS({
|
|
|
25911
26542
|
"use strict";
|
|
25912
26543
|
module.exports = asPromise;
|
|
25913
26544
|
function asPromise(fn, ctx) {
|
|
25914
|
-
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;
|
|
25915
26546
|
while (index < arguments.length)
|
|
25916
26547
|
params[offset++] = arguments[index++];
|
|
25917
26548
|
return new Promise(function executor(resolve11, reject2) {
|
|
25918
26549
|
params[offset] = function callback(err) {
|
|
25919
|
-
if (
|
|
25920
|
-
|
|
26550
|
+
if (pending2) {
|
|
26551
|
+
pending2 = false;
|
|
25921
26552
|
if (err)
|
|
25922
26553
|
reject2(err);
|
|
25923
26554
|
else {
|
|
@@ -25931,8 +26562,8 @@ var require_aspromise = __commonJS({
|
|
|
25931
26562
|
try {
|
|
25932
26563
|
fn.apply(ctx || null, params);
|
|
25933
26564
|
} catch (err) {
|
|
25934
|
-
if (
|
|
25935
|
-
|
|
26565
|
+
if (pending2) {
|
|
26566
|
+
pending2 = false;
|
|
25936
26567
|
reject2(err);
|
|
25937
26568
|
}
|
|
25938
26569
|
}
|
|
@@ -32719,7 +33350,7 @@ __export(processPriority_exports, {
|
|
|
32719
33350
|
bumpProcessPriorityBestEffort: () => bumpProcessPriorityBestEffort,
|
|
32720
33351
|
shouldBumpProcessPriority: () => shouldBumpProcessPriority
|
|
32721
33352
|
});
|
|
32722
|
-
import { spawnSync
|
|
33353
|
+
import { spawnSync } from "child_process";
|
|
32723
33354
|
function shouldBumpProcessPriority(platform2) {
|
|
32724
33355
|
return platform2 === "darwin";
|
|
32725
33356
|
}
|
|
@@ -32731,7 +33362,7 @@ function bumpProcessPriorityBestEffort() {
|
|
|
32731
33362
|
const args = buildTaskpolicyArgs(process.pid);
|
|
32732
33363
|
let result;
|
|
32733
33364
|
try {
|
|
32734
|
-
result =
|
|
33365
|
+
result = spawnSync("taskpolicy", args, { encoding: "utf8", timeout: 2e3 });
|
|
32735
33366
|
} catch (err) {
|
|
32736
33367
|
console.warn(`[priority] taskpolicy spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
32737
33368
|
return false;
|
|
@@ -32758,7 +33389,7 @@ var init_processPriority = __esm({
|
|
|
32758
33389
|
|
|
32759
33390
|
// src/cli.ts
|
|
32760
33391
|
init_backup();
|
|
32761
|
-
import { execFile as
|
|
33392
|
+
import { execFile as execFile7 } from "child_process";
|
|
32762
33393
|
import { existsSync as existsSync30, mkdirSync as mkdirSync18, realpathSync } from "fs";
|
|
32763
33394
|
import { tmpdir as tmpdir3 } from "os";
|
|
32764
33395
|
import { join as join36, resolve as resolve10 } from "path";
|
|
@@ -33197,18 +33828,18 @@ function isDemoMode() {
|
|
|
33197
33828
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
33198
33829
|
var SHELL_PATH_TIMEOUT_MS = 2e3;
|
|
33199
33830
|
function mergePaths(currentPath, shellPath) {
|
|
33200
|
-
const
|
|
33201
|
-
const existing = currentPath.split(
|
|
33831
|
+
const sep2 = ":";
|
|
33832
|
+
const existing = currentPath.split(sep2).filter((s) => s !== "");
|
|
33202
33833
|
const existingSet = new Set(existing);
|
|
33203
33834
|
const additions = [];
|
|
33204
|
-
for (const dir of shellPath.split(
|
|
33835
|
+
for (const dir of shellPath.split(sep2)) {
|
|
33205
33836
|
const trimmed = dir.trim();
|
|
33206
33837
|
if (trimmed === "" || existingSet.has(trimmed)) continue;
|
|
33207
33838
|
existingSet.add(trimmed);
|
|
33208
33839
|
additions.push(trimmed);
|
|
33209
33840
|
}
|
|
33210
33841
|
if (additions.length === 0) return currentPath;
|
|
33211
|
-
return [...additions, ...existing].join(
|
|
33842
|
+
return [...additions, ...existing].join(sep2);
|
|
33212
33843
|
}
|
|
33213
33844
|
function readLoginShellPath(execOverride) {
|
|
33214
33845
|
const shell = process.env.SHELL;
|
|
@@ -33262,7 +33893,7 @@ init_lifecycle2();
|
|
|
33262
33893
|
init_mime_types();
|
|
33263
33894
|
init_projects();
|
|
33264
33895
|
import { serve } from "@hono/node-server";
|
|
33265
|
-
import { execFile as
|
|
33896
|
+
import { execFile as execFile6 } from "child_process";
|
|
33266
33897
|
import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
|
|
33267
33898
|
import { Hono as Hono19 } from "hono";
|
|
33268
33899
|
import { basename as basename5, dirname as dirname8, join as join34 } from "path";
|
|
@@ -33280,7 +33911,7 @@ init_helpers();
|
|
|
33280
33911
|
init_notify();
|
|
33281
33912
|
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
33282
33913
|
import { Hono as Hono2 } from "hono";
|
|
33283
|
-
import { basename as basename3, extname, join as join26, resolve as resolve7 } from "path";
|
|
33914
|
+
import { basename as basename3, extname, join as join26, resolve as resolve7, sep } from "path";
|
|
33284
33915
|
var attachmentRoutes = new Hono2();
|
|
33285
33916
|
attachmentRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
33286
33917
|
const id = parseIntParam(c, "id");
|
|
@@ -33367,7 +33998,7 @@ attachmentRoutes.get("/attachments/file/*", (c) => {
|
|
|
33367
33998
|
const dataDir = c.get("dataDir");
|
|
33368
33999
|
const attachDir = resolve7(join26(dataDir, "attachments"));
|
|
33369
34000
|
const fullPath = resolve7(join26(attachDir, filePath));
|
|
33370
|
-
if (!fullPath.startsWith(attachDir +
|
|
34001
|
+
if (!fullPath.startsWith(attachDir + sep) && fullPath !== attachDir) {
|
|
33371
34002
|
return c.json({ error: "Invalid path" }, 403);
|
|
33372
34003
|
}
|
|
33373
34004
|
if (!existsSync19(fullPath)) {
|
|
@@ -33460,6 +34091,7 @@ function listAliveEntries(dataDir, isPidAlive3 = defaultIsPidAlive) {
|
|
|
33460
34091
|
init_claude_hooks();
|
|
33461
34092
|
init_commandLog();
|
|
33462
34093
|
init_settings();
|
|
34094
|
+
init_ticketWorkIntervals();
|
|
33463
34095
|
init_freezeLogger();
|
|
33464
34096
|
init_file_settings();
|
|
33465
34097
|
init_global_config();
|
|
@@ -33731,7 +34363,10 @@ channelRoutes.post("/channel/permission/dismiss", async (c) => {
|
|
|
33731
34363
|
});
|
|
33732
34364
|
channelRoutes.post("/channel/done", (_c) => {
|
|
33733
34365
|
const secret = _c.get("projectSecret");
|
|
33734
|
-
if (secret)
|
|
34366
|
+
if (secret) {
|
|
34367
|
+
channelDoneFlags.set(secret, true);
|
|
34368
|
+
void closeOpenTicketIntervalsForProject(secret);
|
|
34369
|
+
}
|
|
33735
34370
|
addLogEntry("done", "incoming", "Claude finished", "").catch(() => {
|
|
33736
34371
|
});
|
|
33737
34372
|
notifyChange();
|
|
@@ -33829,6 +34464,7 @@ commandLogRoutes.get("/command-log/count", async (c) => {
|
|
|
33829
34464
|
});
|
|
33830
34465
|
|
|
33831
34466
|
// src/routes/dashboard.ts
|
|
34467
|
+
init_activeProjects();
|
|
33832
34468
|
init_queries();
|
|
33833
34469
|
init_stats();
|
|
33834
34470
|
init_gitignore();
|
|
@@ -33844,6 +34480,7 @@ import { homedir as homedir6, tmpdir } from "os";
|
|
|
33844
34480
|
import { join as join29, relative as relative2, resolve as resolve8 } from "path";
|
|
33845
34481
|
var dashboardRoutes = new Hono5();
|
|
33846
34482
|
dashboardRoutes.get("/poll", async (c) => {
|
|
34483
|
+
markProjectActive(c.get("dataDir"));
|
|
33847
34484
|
const clientVersion = Math.max(0, parseInt(c.req.query("version") ?? "0", 10) || 0);
|
|
33848
34485
|
const changeVersion2 = getChangeVersion();
|
|
33849
34486
|
if (changeVersion2 > clientVersion) {
|
|
@@ -34911,12 +35548,12 @@ import { Hono as Hono13 } from "hono";
|
|
|
34911
35548
|
// src/db/repair.ts
|
|
34912
35549
|
init_backup();
|
|
34913
35550
|
init_pglite();
|
|
34914
|
-
import { execFile as
|
|
35551
|
+
import { execFile as execFile5 } from "child_process";
|
|
34915
35552
|
import { cpSync as cpSync2, existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, rmSync as rmSync10, writeFileSync as writeFileSync18 } from "fs";
|
|
34916
35553
|
import { tmpdir as tmpdir2 } from "os";
|
|
34917
35554
|
import { join as join32 } from "path";
|
|
34918
|
-
import { promisify as
|
|
34919
|
-
var execFileP =
|
|
35555
|
+
import { promisify as promisify3 } from "util";
|
|
35556
|
+
var execFileP = promisify3(execFile5);
|
|
34920
35557
|
async function findWorkingBackup(dataDir) {
|
|
34921
35558
|
const backups = listBackups(dataDir);
|
|
34922
35559
|
for (const backup of backups) {
|
|
@@ -35107,6 +35744,7 @@ dbRoutes.post("/repair/run-pg-resetwal", async (c) => {
|
|
|
35107
35744
|
});
|
|
35108
35745
|
|
|
35109
35746
|
// src/routes/git.ts
|
|
35747
|
+
init_activeProjects();
|
|
35110
35748
|
import { Hono as Hono14 } from "hono";
|
|
35111
35749
|
import { join as join33 } from "path";
|
|
35112
35750
|
|
|
@@ -35168,18 +35806,19 @@ init_watcher();
|
|
|
35168
35806
|
init_gitignore();
|
|
35169
35807
|
init_open_in_file_manager();
|
|
35170
35808
|
var gitRoutes = new Hono14();
|
|
35171
|
-
gitRoutes.get("/git/status", (c) => {
|
|
35809
|
+
gitRoutes.get("/git/status", async (c) => {
|
|
35172
35810
|
const dataDir = c.get("dataDir");
|
|
35811
|
+
markProjectActive(dataDir);
|
|
35173
35812
|
const projectRoot3 = projectRootFromDataDir(dataDir);
|
|
35174
35813
|
const settings = readFileSettings(dataDir);
|
|
35175
35814
|
if (settings.git_tracking_enabled === false) {
|
|
35176
35815
|
return c.json(null);
|
|
35177
35816
|
}
|
|
35178
35817
|
ensureGitWatcher(projectRoot3);
|
|
35179
|
-
const status = getCachedGitStatus(projectRoot3);
|
|
35818
|
+
const status = await getCachedGitStatus(projectRoot3);
|
|
35180
35819
|
if (status === null) return c.json(null);
|
|
35181
35820
|
if (c.req.query("files") === "true") {
|
|
35182
|
-
const files = getGitStatusFiles(projectRoot3);
|
|
35821
|
+
const files = await getGitStatusFiles(projectRoot3);
|
|
35183
35822
|
return c.json({ ...status, files });
|
|
35184
35823
|
}
|
|
35185
35824
|
return c.json(status);
|
|
@@ -35187,14 +35826,14 @@ gitRoutes.get("/git/status", (c) => {
|
|
|
35187
35826
|
function projectRootFromDataDir(dataDir) {
|
|
35188
35827
|
return dataDir.replace(/[\\/]\.hotsheet\/?$/, "");
|
|
35189
35828
|
}
|
|
35190
|
-
gitRoutes.post("/git/fetch", (c) => {
|
|
35829
|
+
gitRoutes.post("/git/fetch", async (c) => {
|
|
35191
35830
|
const dataDir = c.get("dataDir");
|
|
35192
35831
|
const projectRoot3 = projectRootFromDataDir(dataDir);
|
|
35193
35832
|
const settings = readFileSettings(dataDir);
|
|
35194
35833
|
if (settings.git_tracking_enabled === false) {
|
|
35195
35834
|
return c.json({ ok: false, lastFetchedAt: null, error: "git tracking disabled in settings" });
|
|
35196
35835
|
}
|
|
35197
|
-
const result = runGitFetch(projectRoot3);
|
|
35836
|
+
const result = await runGitFetch(projectRoot3);
|
|
35198
35837
|
if (result.ok) dropGitStatusCache(projectRoot3);
|
|
35199
35838
|
return c.json(result);
|
|
35200
35839
|
});
|
|
@@ -38143,54 +38782,62 @@ async function getPromptTimeline(promptId) {
|
|
|
38143
38782
|
spans
|
|
38144
38783
|
};
|
|
38145
38784
|
}
|
|
38146
|
-
async function getPerTicketRollup(ticketNumber) {
|
|
38785
|
+
async function getPerTicketRollup(ticketNumber, secret) {
|
|
38147
38786
|
const db = await getTelemetryDb();
|
|
38148
38787
|
const marker = `hotsheet:ticket=${ticketNumber}`;
|
|
38149
|
-
const
|
|
38150
|
-
|
|
38151
|
-
|
|
38152
|
-
|
|
38153
|
-
|
|
38154
|
-
|
|
38155
|
-
|
|
38156
|
-
|
|
38157
|
-
|
|
38158
|
-
|
|
38159
|
-
|
|
38160
|
-
|
|
38161
|
-
|
|
38162
|
-
|
|
38163
|
-
|
|
38164
|
-
|
|
38165
|
-
|
|
38166
|
-
|
|
38167
|
-
|
|
38168
|
-
|
|
38169
|
-
|
|
38170
|
-
|
|
38171
|
-
|
|
38172
|
-
|
|
38173
|
-
|
|
38174
|
-
|
|
38175
|
-
|
|
38176
|
-
|
|
38177
|
-
|
|
38178
|
-
|
|
38179
|
-
|
|
38180
|
-
|
|
38181
|
-
|
|
38182
|
-
|
|
38183
|
-
|
|
38184
|
-
|
|
38185
|
-
|
|
38186
|
-
|
|
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]
|
|
38187
38833
|
);
|
|
38834
|
+
const row = result.rows[0];
|
|
38188
38835
|
return {
|
|
38189
38836
|
ticketNumber,
|
|
38190
|
-
promptCount:
|
|
38191
|
-
totalCost: Number(
|
|
38192
|
-
totalTokens: Number(
|
|
38193
|
-
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)
|
|
38194
38841
|
};
|
|
38195
38842
|
}
|
|
38196
38843
|
async function getDashboardPayload(window2, timezone = "UTC", allowedSecrets = null, now = /* @__PURE__ */ new Date()) {
|
|
@@ -38272,7 +38919,8 @@ telemetryRoutes.get("/telemetry/prompt/:id", async (c) => {
|
|
|
38272
38919
|
});
|
|
38273
38920
|
telemetryRoutes.get("/telemetry/ticket/:number", async (c) => {
|
|
38274
38921
|
const ticketNumber = c.req.param("number");
|
|
38275
|
-
const
|
|
38922
|
+
const secret = c.get("projectSecret");
|
|
38923
|
+
const rollup = await getPerTicketRollup(ticketNumber, secret);
|
|
38276
38924
|
return c.json(rollup);
|
|
38277
38925
|
});
|
|
38278
38926
|
telemetryRoutes.get("/telemetry/enabled-anywhere", (c) => {
|
|
@@ -38614,7 +39262,7 @@ async function startServer(port, dataDir, options) {
|
|
|
38614
39262
|
`);
|
|
38615
39263
|
if (options?.noOpen !== true) {
|
|
38616
39264
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
38617
|
-
|
|
39265
|
+
execFile6(openCmd, [url2]);
|
|
38618
39266
|
}
|
|
38619
39267
|
return actualPort;
|
|
38620
39268
|
}
|
|
@@ -38792,8 +39440,12 @@ async function startAndConfigure(port, dataDir, strictPort) {
|
|
|
38792
39440
|
const secret = ensureSecret(dataDir, actualPort);
|
|
38793
39441
|
const { bumpProcessPriorityBestEffort: bumpProcessPriorityBestEffort2 } = await Promise.resolve().then(() => (init_processPriority(), processPriority_exports));
|
|
38794
39442
|
bumpProcessPriorityBestEffort2();
|
|
38795
|
-
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));
|
|
38796
39444
|
startServerEventLoopHeartbeat2(dataDir);
|
|
39445
|
+
const { getBackgroundScheduler: getBackgroundScheduler2 } = await Promise.resolve().then(() => (init_backgroundScheduler(), backgroundScheduler_exports));
|
|
39446
|
+
onServerWake2(() => {
|
|
39447
|
+
getBackgroundScheduler2().noteWake();
|
|
39448
|
+
});
|
|
38797
39449
|
initMarkdownSync(dataDir, actualPort);
|
|
38798
39450
|
scheduleAllSync(dataDir);
|
|
38799
39451
|
const { runWithDataDir: runWith } = await Promise.resolve().then(() => (init_connection(), connection_exports));
|
|
@@ -38842,7 +39494,7 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
|
38842
39494
|
if (!noOpen) {
|
|
38843
39495
|
const url2 = `http://localhost:${actualPort}`;
|
|
38844
39496
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
38845
|
-
|
|
39497
|
+
execFile7(openCmd, [url2]);
|
|
38846
39498
|
}
|
|
38847
39499
|
}
|
|
38848
39500
|
async function restorePreviousProjects(dataDir, actualPort) {
|