@andyqiu/codeforge 0.5.11 → 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 +196 -6
- 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);
|
|
@@ -13470,6 +13479,7 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
|
|
|
13470
13479
|
return subject + body + footer;
|
|
13471
13480
|
}
|
|
13472
13481
|
var ORPHAN_GRACE_MS = 60000;
|
|
13482
|
+
var SEMANTIC_ORPHAN_MIN_AGE_MS = 24 * 60 * 60000;
|
|
13473
13483
|
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
13474
13484
|
const keepRecent = opts.keepRecent ?? 50;
|
|
13475
13485
|
if (keepRecent < 0) {
|
|
@@ -13491,7 +13501,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
|
13491
13501
|
return { pruned, kept: kept.length };
|
|
13492
13502
|
});
|
|
13493
13503
|
}
|
|
13494
|
-
async function pruneOrphanWorktrees(mainRoot) {
|
|
13504
|
+
async function pruneOrphanWorktrees(mainRoot, opts = {}) {
|
|
13495
13505
|
const resolved = path13.resolve(mainRoot);
|
|
13496
13506
|
const cleaned = [];
|
|
13497
13507
|
const failed = [];
|
|
@@ -13551,6 +13561,60 @@ async function pruneOrphanWorktrees(mainRoot) {
|
|
|
13551
13561
|
}
|
|
13552
13562
|
}
|
|
13553
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
|
+
}
|
|
13554
13618
|
const codeforgeWorktreeRoot = path13.resolve(path13.join(resolved, DEFAULT_WORKTREE_SUBDIR));
|
|
13555
13619
|
const fsWorktreePaths = [];
|
|
13556
13620
|
try {
|
|
@@ -21710,7 +21774,7 @@ import * as zlib from "node:zlib";
|
|
|
21710
21774
|
// lib/version-injected.ts
|
|
21711
21775
|
function getInjectedVersion() {
|
|
21712
21776
|
try {
|
|
21713
|
-
const v = "0.5.
|
|
21777
|
+
const v = "0.5.12";
|
|
21714
21778
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21715
21779
|
return v;
|
|
21716
21780
|
}
|
|
@@ -22990,9 +23054,9 @@ var handler24 = workflowEngineServer;
|
|
|
22990
23054
|
import path27 from "node:path";
|
|
22991
23055
|
var PLUGIN_NAME25 = "session-worktree-guard";
|
|
22992
23056
|
logLifecycle(PLUGIN_NAME25, "import", {});
|
|
22993
|
-
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/;
|
|
22994
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/;
|
|
22995
|
-
var SIDE_EFFECT_TOKEN_RE = />(
|
|
23059
|
+
var SIDE_EFFECT_TOKEN_RE = />(?![=&])|\|\s*tee\b|\btee\b/;
|
|
22996
23060
|
function isReadOnlyBashCommand(command) {
|
|
22997
23061
|
if (!READ_ONLY_COMMANDS.test(command))
|
|
22998
23062
|
return false;
|
|
@@ -23013,6 +23077,8 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
23013
23077
|
return new RegExp(`git\\b[^\\n]*(?:-C\\s+|--work-tree[=\\s])${esc}`);
|
|
23014
23078
|
}
|
|
23015
23079
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
23080
|
+
var TOUCH_THROTTLE_MS = 5 * 60000;
|
|
23081
|
+
var _touchCache = new Map;
|
|
23016
23082
|
var CLASS_B_CALLER_WHITELIST = new Set([
|
|
23017
23083
|
"codeforge",
|
|
23018
23084
|
"reviewer",
|
|
@@ -23197,6 +23263,22 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23197
23263
|
}
|
|
23198
23264
|
}
|
|
23199
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
|
+
}
|
|
23200
23282
|
if (toolName === "session_merge") {
|
|
23201
23283
|
const action = argsObj["action"];
|
|
23202
23284
|
if (action === "merge") {
|
|
@@ -23425,6 +23507,103 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23425
23507
|
};
|
|
23426
23508
|
var handler25 = sessionWorktreeGuardPlugin;
|
|
23427
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
|
+
|
|
23428
23607
|
// plugins/worktree-lifecycle.ts
|
|
23429
23608
|
var PLUGIN_NAME26 = "worktree-lifecycle";
|
|
23430
23609
|
logLifecycle(PLUGIN_NAME26, "import", {});
|
|
@@ -23435,6 +23614,7 @@ var PRUNE_INTERVAL_MS = 30 * 60000;
|
|
|
23435
23614
|
var lastIdleToastAt = new Map;
|
|
23436
23615
|
var pruneRunning = false;
|
|
23437
23616
|
var _pruneTimer;
|
|
23617
|
+
var _probe = null;
|
|
23438
23618
|
var log15 = makePluginLogger(PLUGIN_NAME26);
|
|
23439
23619
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
23440
23620
|
const mainRoot = ctx.directory;
|
|
@@ -23446,9 +23626,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23446
23626
|
return {};
|
|
23447
23627
|
}
|
|
23448
23628
|
const client = ctx.client;
|
|
23629
|
+
if (_probe) {
|
|
23630
|
+
try {
|
|
23631
|
+
_probe.close();
|
|
23632
|
+
} catch {}
|
|
23633
|
+
}
|
|
23634
|
+
_probe = createSessionProbe();
|
|
23449
23635
|
setImmediate(() => {
|
|
23450
23636
|
safeAsync(PLUGIN_NAME26, "activate.pruneOrphan", async () => {
|
|
23451
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23637
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23638
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23639
|
+
});
|
|
23452
23640
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23453
23641
|
log15.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23454
23642
|
safeWriteLog(PLUGIN_NAME26, {
|
|
@@ -23474,7 +23662,9 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23474
23662
|
pruneRunning = true;
|
|
23475
23663
|
safeAsync(PLUGIN_NAME26, "interval.pruneOrphan", async () => {
|
|
23476
23664
|
try {
|
|
23477
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23665
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23666
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23667
|
+
});
|
|
23478
23668
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23479
23669
|
log15.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23480
23670
|
safeWriteLog(PLUGIN_NAME26, {
|