@andyqiu/codeforge 0.5.10 → 0.5.12
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/index.js +295 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13228,6 +13228,15 @@ async function markPlanReadOk(opts) {
|
|
|
13228
13228
|
return true;
|
|
13229
13229
|
});
|
|
13230
13230
|
}
|
|
13231
|
+
async function touchEntryUpdatedAt(opts) {
|
|
13232
|
+
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
13233
|
+
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
13234
|
+
if (!entry || entry.status !== "active")
|
|
13235
|
+
return false;
|
|
13236
|
+
entry.updatedAt = new Date().toISOString();
|
|
13237
|
+
return true;
|
|
13238
|
+
});
|
|
13239
|
+
}
|
|
13231
13240
|
async function mergeSessionBack(opts) {
|
|
13232
13241
|
const mainRoot = path13.resolve(opts.mainRoot);
|
|
13233
13242
|
const entry = await getSessionWorktree(opts.sessionId, mainRoot);
|
|
@@ -13262,12 +13271,25 @@ async function mergeSessionBack(opts) {
|
|
|
13262
13271
|
}
|
|
13263
13272
|
const hasDevOnce = await packageHasScript(mainRoot, "dev:once");
|
|
13264
13273
|
if (hasDevOnce) {
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13274
|
+
const stagedRaw = await runGit2(mainRoot, [
|
|
13275
|
+
"diff",
|
|
13276
|
+
"--cached",
|
|
13277
|
+
"--name-only",
|
|
13278
|
+
"--diff-filter=ACMR"
|
|
13279
|
+
]);
|
|
13280
|
+
const stagedPaths = stagedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
13281
|
+
const canSkipDevOnce = await shouldSkipDevOnce(mainRoot, stagedPaths);
|
|
13282
|
+
if (canSkipDevOnce) {
|
|
13283
|
+
const sourceCount = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p)).length;
|
|
13284
|
+
console.log(`[session-worktree] skip dev:once: dist 已是最新(${sourceCount} staged 源文件 mtime <= dist mtime)`);
|
|
13285
|
+
} else {
|
|
13286
|
+
try {
|
|
13287
|
+
await runCmd("npm", ["run", "dev:once"], mainRoot);
|
|
13288
|
+
} catch (err) {
|
|
13289
|
+
await runGit2(mainRoot, ["reset", "--hard", "HEAD"]).catch(() => {});
|
|
13290
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13291
|
+
throw new Error(`dev:once 失败已 reset 主仓: ${msg}`);
|
|
13292
|
+
}
|
|
13271
13293
|
}
|
|
13272
13294
|
} else {
|
|
13273
13295
|
console.log(`[session-worktree] skip dev:once: not configured in ${mainRoot}/package.json`);
|
|
@@ -13275,7 +13297,9 @@ async function mergeSessionBack(opts) {
|
|
|
13275
13297
|
const squashedRaw = await runGit2(wt, ["log", "--format=%s", `${baseSha}..HEAD`]);
|
|
13276
13298
|
const squashedCommits = squashedRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
13277
13299
|
const message = opts.commitMessage ?? buildMergeMessage(opts.sessionId, branch, baseSha, squashedCommits);
|
|
13278
|
-
await
|
|
13300
|
+
await runGitWithEnv(mainRoot, ["commit", "-m", message], {
|
|
13301
|
+
SKIP_DEV_SYNC_CHECK: "1"
|
|
13302
|
+
});
|
|
13279
13303
|
const newSha = (await runGit2(mainRoot, ["rev-parse", "HEAD"])).trim();
|
|
13280
13304
|
try {
|
|
13281
13305
|
await removeWorktree({ root: mainRoot, worktree_path: wt, force: true });
|
|
@@ -13376,6 +13400,24 @@ function runGit2(cwd, args, timeoutMs = 1e4) {
|
|
|
13376
13400
|
});
|
|
13377
13401
|
});
|
|
13378
13402
|
}
|
|
13403
|
+
function runGitWithEnv(cwd, args, envOverrides, timeoutMs = 1e4) {
|
|
13404
|
+
const inheritedEnv = process["env"];
|
|
13405
|
+
return new Promise((resolve11, reject) => {
|
|
13406
|
+
execFile3("git", args, {
|
|
13407
|
+
cwd,
|
|
13408
|
+
timeout: timeoutMs,
|
|
13409
|
+
windowsHide: true,
|
|
13410
|
+
encoding: "utf8",
|
|
13411
|
+
env: Object.assign({}, inheritedEnv, envOverrides)
|
|
13412
|
+
}, (err, stdout, stderr) => {
|
|
13413
|
+
if (err) {
|
|
13414
|
+
reject(new Error(`git ${args.join(" ")} (cwd=${cwd}) 失败: ${stderr?.trim() || err.message}`));
|
|
13415
|
+
return;
|
|
13416
|
+
}
|
|
13417
|
+
resolve11(stdout);
|
|
13418
|
+
});
|
|
13419
|
+
});
|
|
13420
|
+
}
|
|
13379
13421
|
async function packageHasScript(mainRoot, scriptName) {
|
|
13380
13422
|
try {
|
|
13381
13423
|
const pkgPath = path13.join(mainRoot, "package.json");
|
|
@@ -13388,7 +13430,31 @@ async function packageHasScript(mainRoot, scriptName) {
|
|
|
13388
13430
|
return false;
|
|
13389
13431
|
}
|
|
13390
13432
|
}
|
|
13391
|
-
function
|
|
13433
|
+
async function shouldSkipDevOnce(mainRoot, stagedPaths) {
|
|
13434
|
+
let distMtimeSec;
|
|
13435
|
+
try {
|
|
13436
|
+
const st = await fs10.stat(path13.join(mainRoot, "dist/index.js"));
|
|
13437
|
+
distMtimeSec = Math.floor(st.mtimeMs / 1000);
|
|
13438
|
+
} catch {
|
|
13439
|
+
return false;
|
|
13440
|
+
}
|
|
13441
|
+
const relevant = stagedPaths.filter((p) => /^(plugins|lib|src)\//.test(p) && !/\.(md|test\.ts)$/.test(p));
|
|
13442
|
+
if (relevant.length === 0)
|
|
13443
|
+
return true;
|
|
13444
|
+
for (const rel of relevant) {
|
|
13445
|
+
try {
|
|
13446
|
+
const abs = path13.join(mainRoot, rel);
|
|
13447
|
+
const st = await fs10.stat(abs);
|
|
13448
|
+
const srcMtimeSec = Math.floor(st.mtimeMs / 1000);
|
|
13449
|
+
if (srcMtimeSec > distMtimeSec)
|
|
13450
|
+
return false;
|
|
13451
|
+
} catch {
|
|
13452
|
+
return false;
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
return true;
|
|
13456
|
+
}
|
|
13457
|
+
function runCmd(cmd, args, cwd, timeoutMs = 300000) {
|
|
13392
13458
|
return new Promise((resolve11, reject) => {
|
|
13393
13459
|
execFile3(cmd, args, { cwd, timeout: timeoutMs, windowsHide: true, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
13394
13460
|
if (err) {
|
|
@@ -13413,6 +13479,7 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
|
|
|
13413
13479
|
return subject + body + footer;
|
|
13414
13480
|
}
|
|
13415
13481
|
var ORPHAN_GRACE_MS = 60000;
|
|
13482
|
+
var SEMANTIC_ORPHAN_MIN_AGE_MS = 24 * 60 * 60000;
|
|
13416
13483
|
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
13417
13484
|
const keepRecent = opts.keepRecent ?? 50;
|
|
13418
13485
|
if (keepRecent < 0) {
|
|
@@ -13434,7 +13501,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
|
13434
13501
|
return { pruned, kept: kept.length };
|
|
13435
13502
|
});
|
|
13436
13503
|
}
|
|
13437
|
-
async function pruneOrphanWorktrees(mainRoot) {
|
|
13504
|
+
async function pruneOrphanWorktrees(mainRoot, opts = {}) {
|
|
13438
13505
|
const resolved = path13.resolve(mainRoot);
|
|
13439
13506
|
const cleaned = [];
|
|
13440
13507
|
const failed = [];
|
|
@@ -13494,6 +13561,60 @@ async function pruneOrphanWorktrees(mainRoot) {
|
|
|
13494
13561
|
}
|
|
13495
13562
|
}
|
|
13496
13563
|
});
|
|
13564
|
+
if (opts.isSessionAlive) {
|
|
13565
|
+
const minAge = opts.semanticOrphanMinAgeMs ?? SEMANTIC_ORPHAN_MIN_AGE_MS;
|
|
13566
|
+
const probe = opts.isSessionAlive;
|
|
13567
|
+
await mutateRegistry(resolved, async (reg2) => {
|
|
13568
|
+
const now = Date.now();
|
|
13569
|
+
for (const entry of reg2.entries) {
|
|
13570
|
+
if (entry.status !== "active")
|
|
13571
|
+
continue;
|
|
13572
|
+
const updatedMs = Date.parse(entry.updatedAt);
|
|
13573
|
+
if (!Number.isFinite(updatedMs) || now - updatedMs < minAge) {
|
|
13574
|
+
skipped++;
|
|
13575
|
+
continue;
|
|
13576
|
+
}
|
|
13577
|
+
let aliveResult;
|
|
13578
|
+
try {
|
|
13579
|
+
aliveResult = await probe(entry.sessionId);
|
|
13580
|
+
} catch {
|
|
13581
|
+
skipped++;
|
|
13582
|
+
continue;
|
|
13583
|
+
}
|
|
13584
|
+
if (aliveResult.source === "unknown") {
|
|
13585
|
+
skipped++;
|
|
13586
|
+
continue;
|
|
13587
|
+
}
|
|
13588
|
+
if (aliveResult.alive) {
|
|
13589
|
+
skipped++;
|
|
13590
|
+
continue;
|
|
13591
|
+
}
|
|
13592
|
+
try {
|
|
13593
|
+
await removeWorktree({
|
|
13594
|
+
root: resolved,
|
|
13595
|
+
worktree_path: entry.worktreePath,
|
|
13596
|
+
force: true
|
|
13597
|
+
});
|
|
13598
|
+
} catch (err) {
|
|
13599
|
+
failed.push({
|
|
13600
|
+
worktreePath: entry.worktreePath,
|
|
13601
|
+
error: `D 类 removeWorktree 失败: ${err instanceof Error ? err.message : String(err)}`
|
|
13602
|
+
});
|
|
13603
|
+
continue;
|
|
13604
|
+
}
|
|
13605
|
+
try {
|
|
13606
|
+
await runGit2(resolved, ["branch", "-D", entry.branch]);
|
|
13607
|
+
} catch {}
|
|
13608
|
+
entry.status = "discarded";
|
|
13609
|
+
entry.updatedAt = new Date().toISOString();
|
|
13610
|
+
cleaned.push({
|
|
13611
|
+
sessionId: entry.sessionId,
|
|
13612
|
+
worktreePath: entry.worktreePath,
|
|
13613
|
+
reason: `D 类语义孤儿 (opencode session ${aliveResult.source}: dead)`
|
|
13614
|
+
});
|
|
13615
|
+
}
|
|
13616
|
+
});
|
|
13617
|
+
}
|
|
13497
13618
|
const codeforgeWorktreeRoot = path13.resolve(path13.join(resolved, DEFAULT_WORKTREE_SUBDIR));
|
|
13498
13619
|
const fsWorktreePaths = [];
|
|
13499
13620
|
try {
|
|
@@ -21232,7 +21353,11 @@ var RISK_PATTERNS = [
|
|
|
21232
21353
|
kinds: ["bash", "other"],
|
|
21233
21354
|
re: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE|DROP\s+SCHEMA)\b/i
|
|
21234
21355
|
},
|
|
21235
|
-
{
|
|
21356
|
+
{
|
|
21357
|
+
tag: "write_secrets",
|
|
21358
|
+
re: /(\.env(?:\.\w+)?|id_[edr]sa|\.ssh\/id_|\.pem|\.p12|secret\.json)/i,
|
|
21359
|
+
matchOn: ["command", "filePath", "path"]
|
|
21360
|
+
},
|
|
21236
21361
|
{
|
|
21237
21362
|
tag: "write_etc",
|
|
21238
21363
|
kinds: ["bash", "edit"],
|
|
@@ -21313,11 +21438,11 @@ function classifyTool(tool2) {
|
|
|
21313
21438
|
}
|
|
21314
21439
|
function evaluateRisk(tool2, args) {
|
|
21315
21440
|
const kind = classifyTool(tool2);
|
|
21316
|
-
const haystack = buildHaystack(args);
|
|
21317
21441
|
const hits = [];
|
|
21318
21442
|
for (const pattern of RISK_PATTERNS) {
|
|
21319
21443
|
if (pattern.kinds && !pattern.kinds.includes(kind))
|
|
21320
21444
|
continue;
|
|
21445
|
+
const haystack = buildHaystackFor(args, pattern.matchOn);
|
|
21321
21446
|
const m = haystack.match(pattern.re);
|
|
21322
21447
|
if (m) {
|
|
21323
21448
|
hits.push({
|
|
@@ -21338,6 +21463,19 @@ function buildHaystack(args) {
|
|
|
21338
21463
|
return String(args);
|
|
21339
21464
|
}
|
|
21340
21465
|
}
|
|
21466
|
+
function buildHaystackFor(args, matchOn) {
|
|
21467
|
+
if (!matchOn || matchOn.length === 0) {
|
|
21468
|
+
return buildHaystack(args);
|
|
21469
|
+
}
|
|
21470
|
+
const parts = [];
|
|
21471
|
+
for (const key of matchOn) {
|
|
21472
|
+
if (key in args) {
|
|
21473
|
+
const val = args[key];
|
|
21474
|
+
parts.push(typeof val === "string" ? val : JSON.stringify(val));
|
|
21475
|
+
}
|
|
21476
|
+
}
|
|
21477
|
+
return parts.join(" ");
|
|
21478
|
+
}
|
|
21341
21479
|
|
|
21342
21480
|
// lib/file-regex-acl.ts
|
|
21343
21481
|
import * as path23 from "node:path";
|
|
@@ -21636,7 +21774,7 @@ import * as zlib from "node:zlib";
|
|
|
21636
21774
|
// lib/version-injected.ts
|
|
21637
21775
|
function getInjectedVersion() {
|
|
21638
21776
|
try {
|
|
21639
|
-
const v = "0.5.
|
|
21777
|
+
const v = "0.5.12";
|
|
21640
21778
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21641
21779
|
return v;
|
|
21642
21780
|
}
|
|
@@ -22916,9 +23054,9 @@ var handler24 = workflowEngineServer;
|
|
|
22916
23054
|
import path27 from "node:path";
|
|
22917
23055
|
var PLUGIN_NAME25 = "session-worktree-guard";
|
|
22918
23056
|
logLifecycle(PLUGIN_NAME25, "import", {});
|
|
22919
|
-
var WRITE_INTENT_RE = />(
|
|
23057
|
+
var WRITE_INTENT_RE = />(?![=&])|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
22920
23058
|
var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
|
|
22921
|
-
var SIDE_EFFECT_TOKEN_RE = />(
|
|
23059
|
+
var SIDE_EFFECT_TOKEN_RE = />(?![=&])|\|\s*tee\b|\btee\b/;
|
|
22922
23060
|
function isReadOnlyBashCommand(command) {
|
|
22923
23061
|
if (!READ_ONLY_COMMANDS.test(command))
|
|
22924
23062
|
return false;
|
|
@@ -22939,6 +23077,8 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
22939
23077
|
return new RegExp(`git\\b[^\\n]*(?:-C\\s+|--work-tree[=\\s])${esc}`);
|
|
22940
23078
|
}
|
|
22941
23079
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
23080
|
+
var TOUCH_THROTTLE_MS = 5 * 60000;
|
|
23081
|
+
var _touchCache = new Map;
|
|
22942
23082
|
var CLASS_B_CALLER_WHITELIST = new Set([
|
|
22943
23083
|
"codeforge",
|
|
22944
23084
|
"reviewer",
|
|
@@ -22970,6 +23110,20 @@ function commandContainsMainRoot(command, mainRoot) {
|
|
|
22970
23110
|
const re = new RegExp(`${escapeRegex2(mainRoot)}(?=[\\s'"\`)]|$)`);
|
|
22971
23111
|
return re.test(command);
|
|
22972
23112
|
}
|
|
23113
|
+
function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) {
|
|
23114
|
+
if (!worktreePath || worktreePath === mainRoot) {
|
|
23115
|
+
return commandContainsMainRoot(command, mainRoot);
|
|
23116
|
+
}
|
|
23117
|
+
const wtpIdx = command.indexOf(worktreePath);
|
|
23118
|
+
if (wtpIdx !== -1) {
|
|
23119
|
+
const afterWtp = command.slice(wtpIdx + worktreePath.length);
|
|
23120
|
+
if (/(?:^|[/\\])\.\.(?:[/\\\s'";|<>]|$)/.test(afterWtp)) {
|
|
23121
|
+
return true;
|
|
23122
|
+
}
|
|
23123
|
+
}
|
|
23124
|
+
const sanitized = command.split(worktreePath).join("");
|
|
23125
|
+
return commandContainsMainRoot(sanitized, mainRoot);
|
|
23126
|
+
}
|
|
22973
23127
|
function detectBashWriteIntent(command, mainRoot) {
|
|
22974
23128
|
if (isReadOnlyBashCommand(command))
|
|
22975
23129
|
return false;
|
|
@@ -23109,6 +23263,22 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23109
23263
|
}
|
|
23110
23264
|
}
|
|
23111
23265
|
const worktreePath = entry.worktreePath;
|
|
23266
|
+
if (entry.status === "active" && isWriteOperation(toolName, argsObj, mainRoot)) {
|
|
23267
|
+
const last = _touchCache.get(entry.sessionId) ?? 0;
|
|
23268
|
+
const nowMs = Date.now();
|
|
23269
|
+
if (nowMs - last > TOUCH_THROTTLE_MS) {
|
|
23270
|
+
_touchCache.set(entry.sessionId, nowMs);
|
|
23271
|
+
touchEntryUpdatedAt({
|
|
23272
|
+
sessionId: entry.sessionId,
|
|
23273
|
+
mainRoot
|
|
23274
|
+
}).catch((err) => {
|
|
23275
|
+
log14.warn("touchEntryUpdatedAt 失败 (已忽略)", {
|
|
23276
|
+
sessionId: entry?.sessionId,
|
|
23277
|
+
error: err instanceof Error ? err.message : String(err)
|
|
23278
|
+
});
|
|
23279
|
+
});
|
|
23280
|
+
}
|
|
23281
|
+
}
|
|
23112
23282
|
if (toolName === "session_merge") {
|
|
23113
23283
|
const action = argsObj["action"];
|
|
23114
23284
|
if (action === "merge") {
|
|
@@ -23227,7 +23397,7 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23227
23397
|
}
|
|
23228
23398
|
if (toolName === "bash") {
|
|
23229
23399
|
const command = argsObj["command"];
|
|
23230
|
-
if (typeof command === "string" &&
|
|
23400
|
+
if (typeof command === "string" && commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePath) && detectBashWriteIntent(command, mainRoot)) {
|
|
23231
23401
|
const caller = await resolveAgentForGuard({ sessionID: input.sessionID, agent: input.agent }, ctx.client, log14);
|
|
23232
23402
|
if (caller !== null && CLASS_B_CALLER_WHITELIST.has(caller)) {
|
|
23233
23403
|
log14.debug?.(`[class-b-whitelist] allow caller=${caller}`, { sessionId, tool: toolName, command: command.slice(0, 200) });
|
|
@@ -23337,6 +23507,103 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23337
23507
|
};
|
|
23338
23508
|
var handler25 = sessionWorktreeGuardPlugin;
|
|
23339
23509
|
|
|
23510
|
+
// lib/opencode-session-probe.ts
|
|
23511
|
+
import * as path28 from "node:path";
|
|
23512
|
+
import * as os6 from "node:os";
|
|
23513
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
23514
|
+
var requireFromHere = createRequire2(import.meta.url);
|
|
23515
|
+
var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
|
|
23516
|
+
var DEFAULT_DB_PATH = path28.join(os6.homedir(), ".local/share/opencode/opencode.db");
|
|
23517
|
+
function createSessionProbe(opts = {}) {
|
|
23518
|
+
const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
|
|
23519
|
+
const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
|
|
23520
|
+
const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
|
|
23521
|
+
const timeoutMs = opts.timeoutMs ?? 1500;
|
|
23522
|
+
let db = null;
|
|
23523
|
+
let dbInitTried = false;
|
|
23524
|
+
let dbStmt = null;
|
|
23525
|
+
async function tryOpenDb() {
|
|
23526
|
+
if (dbInitTried)
|
|
23527
|
+
return db != null;
|
|
23528
|
+
dbInitTried = true;
|
|
23529
|
+
try {
|
|
23530
|
+
const mod = requireFromHere("node:sqlite");
|
|
23531
|
+
try {
|
|
23532
|
+
db = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
23533
|
+
} catch {
|
|
23534
|
+
db = null;
|
|
23535
|
+
dbStmt = null;
|
|
23536
|
+
return false;
|
|
23537
|
+
}
|
|
23538
|
+
dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
|
|
23539
|
+
return true;
|
|
23540
|
+
} catch {
|
|
23541
|
+
db = null;
|
|
23542
|
+
dbStmt = null;
|
|
23543
|
+
return false;
|
|
23544
|
+
}
|
|
23545
|
+
}
|
|
23546
|
+
async function probeHttp(sessionId) {
|
|
23547
|
+
if (!httpBaseUrl)
|
|
23548
|
+
return null;
|
|
23549
|
+
try {
|
|
23550
|
+
const ctrl = new AbortController;
|
|
23551
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
23552
|
+
const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
|
|
23553
|
+
signal: ctrl.signal
|
|
23554
|
+
}).finally(() => clearTimeout(t));
|
|
23555
|
+
if (!res.ok)
|
|
23556
|
+
return null;
|
|
23557
|
+
const list = await res.json();
|
|
23558
|
+
const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
|
|
23559
|
+
return { alive: hit, source: "http" };
|
|
23560
|
+
} catch {
|
|
23561
|
+
return null;
|
|
23562
|
+
}
|
|
23563
|
+
}
|
|
23564
|
+
async function probeSqlite(sessionId) {
|
|
23565
|
+
if (!await tryOpenDb() || !dbStmt)
|
|
23566
|
+
return null;
|
|
23567
|
+
try {
|
|
23568
|
+
const row = dbStmt.get(sessionId);
|
|
23569
|
+
if (!row) {
|
|
23570
|
+
return { alive: false, source: "sqlite" };
|
|
23571
|
+
}
|
|
23572
|
+
if (row.time_archived != null) {
|
|
23573
|
+
return {
|
|
23574
|
+
alive: false,
|
|
23575
|
+
source: "sqlite",
|
|
23576
|
+
time_archived: row.time_archived
|
|
23577
|
+
};
|
|
23578
|
+
}
|
|
23579
|
+
const now = Date.now();
|
|
23580
|
+
const tu = Number(row.time_updated) || 0;
|
|
23581
|
+
const alive = now - tu < livenessWindowMs;
|
|
23582
|
+
return { alive, source: "sqlite", time_updated: tu, time_archived: null };
|
|
23583
|
+
} catch {
|
|
23584
|
+
return null;
|
|
23585
|
+
}
|
|
23586
|
+
}
|
|
23587
|
+
return {
|
|
23588
|
+
async isSessionAlive(sessionId) {
|
|
23589
|
+
const http = await probeHttp(sessionId);
|
|
23590
|
+
if (http)
|
|
23591
|
+
return http;
|
|
23592
|
+
const sql = await probeSqlite(sessionId);
|
|
23593
|
+
if (sql)
|
|
23594
|
+
return sql;
|
|
23595
|
+
return { alive: true, source: "unknown" };
|
|
23596
|
+
},
|
|
23597
|
+
close() {
|
|
23598
|
+
try {
|
|
23599
|
+
db?.close?.();
|
|
23600
|
+
} catch {}
|
|
23601
|
+
db = null;
|
|
23602
|
+
dbStmt = null;
|
|
23603
|
+
}
|
|
23604
|
+
};
|
|
23605
|
+
}
|
|
23606
|
+
|
|
23340
23607
|
// plugins/worktree-lifecycle.ts
|
|
23341
23608
|
var PLUGIN_NAME26 = "worktree-lifecycle";
|
|
23342
23609
|
logLifecycle(PLUGIN_NAME26, "import", {});
|
|
@@ -23347,6 +23614,7 @@ var PRUNE_INTERVAL_MS = 30 * 60000;
|
|
|
23347
23614
|
var lastIdleToastAt = new Map;
|
|
23348
23615
|
var pruneRunning = false;
|
|
23349
23616
|
var _pruneTimer;
|
|
23617
|
+
var _probe = null;
|
|
23350
23618
|
var log15 = makePluginLogger(PLUGIN_NAME26);
|
|
23351
23619
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
23352
23620
|
const mainRoot = ctx.directory;
|
|
@@ -23358,9 +23626,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23358
23626
|
return {};
|
|
23359
23627
|
}
|
|
23360
23628
|
const client = ctx.client;
|
|
23629
|
+
if (_probe) {
|
|
23630
|
+
try {
|
|
23631
|
+
_probe.close();
|
|
23632
|
+
} catch {}
|
|
23633
|
+
}
|
|
23634
|
+
_probe = createSessionProbe();
|
|
23361
23635
|
setImmediate(() => {
|
|
23362
23636
|
safeAsync(PLUGIN_NAME26, "activate.pruneOrphan", async () => {
|
|
23363
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23637
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23638
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23639
|
+
});
|
|
23364
23640
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23365
23641
|
log15.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23366
23642
|
safeWriteLog(PLUGIN_NAME26, {
|
|
@@ -23386,7 +23662,9 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23386
23662
|
pruneRunning = true;
|
|
23387
23663
|
safeAsync(PLUGIN_NAME26, "interval.pruneOrphan", async () => {
|
|
23388
23664
|
try {
|
|
23389
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23665
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23666
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23667
|
+
});
|
|
23390
23668
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23391
23669
|
log15.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23392
23670
|
safeWriteLog(PLUGIN_NAME26, {
|