@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.
Files changed (2) hide show
  1. package/dist/index.js +196 -6
  2. 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.11";
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 = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
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 = />(?!=)|\|\s*tee\b|\btee\b/;
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, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andyqiu/codeforge",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "CodeForge — opencode 的零侵入扩展包",
5
5
  "type": "module",
6
6
  "private": false,