larkway 0.3.28 → 0.3.29

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.28**
11
+ **Current release: v0.3.29**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.28**
11
+ **当前版本:v0.3.29**
12
12
 
13
13
  ---
14
14
 
package/dist/main.js CHANGED
@@ -115875,6 +115875,10 @@ function resolveAgentWorkspacePath(agentId) {
115875
115875
  function resolveAgentWorkspaceSessionsDir(agentId) {
115876
115876
  return join(resolveAgentWorkspacePath(agentId), "sessions");
115877
115877
  }
115878
+ function resolveAgentSessionPath(agentId, threadId) {
115879
+ assertSafePathSegment("threadId", threadId);
115880
+ return join(resolveAgentWorkspaceSessionsDir(agentId), threadId);
115881
+ }
115878
115882
  function resolveAgentWorkspaceReposDir(agentId) {
115879
115883
  return join(resolveAgentWorkspacePath(agentId), "repos");
115880
115884
  }
@@ -121398,6 +121402,13 @@ var BridgeHandler = class {
121398
121402
  botGitIdentity: this.deps.botConfig?.git_identity,
121399
121403
  gitlabToken: this.deps.gitlabToken
121400
121404
  });
121405
+ if (isAgentWorkspace && handle.pid != null) {
121406
+ const sessionPidFile = path9.join(worktreePath, ".larkway", "runner.pid");
121407
+ void fs6.mkdir(path9.dirname(sessionPidFile), { recursive: true }).then(
121408
+ () => fs6.writeFile(sessionPidFile, JSON.stringify({ pid: handle.pid }), "utf8")
121409
+ ).catch(() => {
121410
+ });
121411
+ }
121401
121412
  let sessionId;
121402
121413
  let trustedAnswerText = "";
121403
121414
  try {
@@ -121793,7 +121804,7 @@ function isRuntimeEventRecord(value) {
121793
121804
 
121794
121805
  // src/housekeeping/gc.ts
121795
121806
  import { spawn } from "node:child_process";
121796
- import { readFile as readFile4, readdir, stat } from "node:fs/promises";
121807
+ import { readFile as readFile4, readdir, rm, stat } from "node:fs/promises";
121797
121808
  import { join as pathJoin } from "node:path";
121798
121809
  var DEFAULT_SCAN_INTERVAL_MS = 30 * 60 * 1e3;
121799
121810
  var DEFAULT_IDLE_NOTIFY_MS = 4 * 60 * 60 * 1e3;
@@ -121805,12 +121816,20 @@ var Housekeeping = class {
121805
121816
  #idleCleanupMs;
121806
121817
  /** This housekeeping's bot scope — resolves which worktrees dir to sweep. */
121807
121818
  #botId;
121819
+ /**
121820
+ * Bot runtime. "agent_workspace" reclaims per-thread session dirs under
121821
+ * agents/<id>/workspace/sessions/ via rm -rf (they are plain dirs / full
121822
+ * clones, not git worktrees). Anything else (legacy) reclaims git worktrees
121823
+ * under <botId>/worktrees/ via `git worktree remove`.
121824
+ */
121825
+ #runtime;
121808
121826
  /** thread_ids that have already received an idle-notify warn this session */
121809
121827
  #notified = /* @__PURE__ */ new Set();
121810
121828
  #timer;
121811
121829
  constructor(deps, opts) {
121812
121830
  this.#sessionStore = deps.sessionStore;
121813
121831
  this.#botId = deps.botId;
121832
+ this.#runtime = deps.runtime;
121814
121833
  this.#scanIntervalMs = opts?.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
121815
121834
  this.#idleNotifyMs = opts?.idleNotifyMs ?? DEFAULT_IDLE_NOTIFY_MS;
121816
121835
  this.#idleCleanupMs = opts?.idleCleanupMs ?? DEFAULT_IDLE_CLEANUP_MS;
@@ -121855,9 +121874,9 @@ var Housekeeping = class {
121855
121874
  if (idleMs >= this.#idleCleanupMs) {
121856
121875
  const idleHours = Math.floor(idleMs / (60 * 60 * 1e3));
121857
121876
  console.warn(
121858
- `[housekeeping] \u8BDD\u9898 ${tid} idle ${idleHours}h+,worktree \u53EF\u6E05\u7406`
121877
+ `[housekeeping] \u8BDD\u9898 ${tid} idle ${idleHours}h+,\u5DE5\u4F5C\u76EE\u5F55\u53EF\u6E05\u7406`
121859
121878
  );
121860
- void cleanupWorktree(tid, record.botId, dryRun);
121879
+ void this.#cleanupThread(tid, record.botId, dryRun);
121861
121880
  this.#notified.delete(tid);
121862
121881
  continue;
121863
121882
  }
@@ -121878,33 +121897,47 @@ var Housekeeping = class {
121878
121897
  * cleanupWorktree (kill PIDs → git worktree remove --force).
121879
121898
  */
121880
121899
  async #sweepOrphans(liveThreadIds, now, dryRun) {
121881
- const worktreesDir = resolveWorktreesDir(this.#botId);
121900
+ const reclaimDir = this.#runtime === "agent_workspace" ? resolveAgentWorkspaceSessionsDir(this.#botId ?? "") : resolveWorktreesDir(this.#botId);
121882
121901
  let dirNames;
121883
121902
  try {
121884
- const entries = await readdir(worktreesDir, { withFileTypes: true });
121903
+ const entries = await readdir(reclaimDir, { withFileTypes: true });
121885
121904
  dirNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
121886
121905
  } catch (err) {
121887
121906
  if (err.code !== "ENOENT") {
121888
- console.error(`[gc] orphan sweep: cannot read ${worktreesDir}:`, err);
121907
+ console.error(`[gc] orphan sweep: cannot read ${reclaimDir}:`, err);
121889
121908
  }
121890
121909
  return;
121891
121910
  }
121892
121911
  for (const name of selectOrphanWorktreeNames(dirNames, liveThreadIds)) {
121893
- const wtPath = pathJoin(worktreesDir, name);
121912
+ if (name.startsWith("_")) continue;
121913
+ const dirPath = pathJoin(reclaimDir, name);
121894
121914
  let ageMs;
121895
121915
  try {
121896
- ageMs = now - (await stat(wtPath)).mtimeMs;
121916
+ ageMs = now - (await stat(dirPath)).mtimeMs;
121897
121917
  } catch {
121898
121918
  continue;
121899
121919
  }
121900
121920
  if (ageMs < this.#idleCleanupMs) continue;
121901
121921
  const ageHours = Math.floor(ageMs / (60 * 60 * 1e3));
121902
121922
  console.warn(
121903
- `[housekeeping] orphan worktree ${name}(\u65E0 session \u8BB0\u5F55, idle ${ageHours}h+)\u2014 \u6E05\u7406`
121923
+ `[housekeeping] orphan ${name}(\u65E0 session \u8BB0\u5F55, idle ${ageHours}h+)\u2014 \u6E05\u7406`
121904
121924
  );
121905
- await cleanupWorktree(name, this.#botId, dryRun);
121925
+ await this.#cleanupThread(name, this.#botId, dryRun);
121906
121926
  }
121907
121927
  }
121928
+ /**
121929
+ * Runtime-aware reclaim of one thread's working dir.
121930
+ * - agent_workspace: rm -rf agents/<id>/workspace/sessions/<tid> (plain dir /
121931
+ * full clones — `git worktree remove` cannot reclaim these).
121932
+ * - legacy: git worktree remove --force <botId>/worktrees/<tid>.
121933
+ * Both kill any lingering runner PIDs first (idle >24h → normally dead).
121934
+ */
121935
+ #cleanupThread(threadId, botId, dryRun) {
121936
+ if (this.#runtime === "agent_workspace") {
121937
+ return cleanupAgentSession(threadId, botId ?? this.#botId, dryRun);
121938
+ }
121939
+ return cleanupWorktree(threadId, botId, dryRun);
121940
+ }
121908
121941
  };
121909
121942
  function selectOrphanWorktreeNames(dirNames, liveThreadIds) {
121910
121943
  return dirNames.filter((name) => !liveThreadIds.has(name));
@@ -122034,6 +122067,61 @@ async function cleanupWorktree(threadId, botId, dryRun) {
122034
122067
  console.error(`[gc] worktree remove failed for path=${worktreePath}:`, err);
122035
122068
  }
122036
122069
  }
122070
+ function isReclaimableSessionPath(p) {
122071
+ const m = /[/\\]workspace[/\\]sessions[/\\]([^/\\]+)[/\\]?$/.exec(p);
122072
+ if (m === null) return false;
122073
+ const seg = m[1];
122074
+ return seg !== "." && seg !== "..";
122075
+ }
122076
+ async function removeSessionDir(sessionPath, dryRun) {
122077
+ if (!isReclaimableSessionPath(sessionPath)) {
122078
+ console.error(`[gc] refusing to rm -rf non-session path: ${sessionPath}`);
122079
+ return;
122080
+ }
122081
+ if (dryRun) {
122082
+ console.log(`[gc] dry-run: would rm -rf ${sessionPath}`);
122083
+ return;
122084
+ }
122085
+ console.log(`[gc] rm -rf ${sessionPath}`);
122086
+ try {
122087
+ await rm(sessionPath, { recursive: true, force: true });
122088
+ } catch (err) {
122089
+ console.error(`[gc] rm -rf failed for ${sessionPath}:`, err);
122090
+ }
122091
+ }
122092
+ async function cleanupAgentSession(threadId, agentId, dryRun) {
122093
+ if (!agentId) {
122094
+ console.error(
122095
+ `[gc] cleanupAgentSession: missing agentId for thread=${threadId}`
122096
+ );
122097
+ return;
122098
+ }
122099
+ let sessionPath;
122100
+ try {
122101
+ sessionPath = resolveAgentSessionPath(agentId, threadId);
122102
+ } catch (err) {
122103
+ console.error(`[gc] invalid session threadId=${threadId}:`, err);
122104
+ return;
122105
+ }
122106
+ console.log(
122107
+ `[gc] cleanup session thread=${threadId} path=${sessionPath} dryRun=${dryRun}`
122108
+ );
122109
+ let pids;
122110
+ try {
122111
+ pids = await findPidsByWorktree(sessionPath);
122112
+ } catch (err) {
122113
+ console.error(`[gc] pid lookup failed for path=${sessionPath}:`, err);
122114
+ pids = [];
122115
+ }
122116
+ const alivePids = pids.filter(isPidAlive);
122117
+ if (alivePids.length > 0) {
122118
+ console.warn(
122119
+ `[gc] skip live session thread=${threadId} path=${sessionPath}: runner pid(s) [${alivePids.join(", ")}] still alive \u2014 not reclaiming in-flight work`
122120
+ );
122121
+ return;
122122
+ }
122123
+ await removeSessionDir(sessionPath, dryRun);
122124
+ }
122037
122125
 
122038
122126
  // src/config/botLoader.ts
122039
122127
  import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
@@ -126008,7 +126096,8 @@ stderr: ${stderr}` : "")
126008
126096
  return {
126009
126097
  events: generateEvents(),
126010
126098
  done,
126011
- kill: doKill
126099
+ kill: doKill,
126100
+ pid: child.pid ?? void 0
126012
126101
  };
126013
126102
  }
126014
126103
  var ClaudeRunner = class {
@@ -126365,7 +126454,8 @@ stderr: ${stderr}` : "")
126365
126454
  return {
126366
126455
  events: generateEvents(),
126367
126456
  done,
126368
- kill: doKill
126457
+ kill: doKill,
126458
+ pid: child.pid ?? void 0
126369
126459
  };
126370
126460
  }
126371
126461
  var CodexRunner = class {
@@ -126839,7 +126929,11 @@ async function runV2Mode({
126839
126929
  await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
126840
126930
  }
126841
126931
  });
126842
- const housekeeping = new Housekeeping({ sessionStore, botId: bot.id });
126932
+ const housekeeping = new Housekeeping({
126933
+ sessionStore,
126934
+ botId: bot.id,
126935
+ runtime: bot.runtime
126936
+ });
126843
126937
  const inst = {
126844
126938
  bot,
126845
126939
  client,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",