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 +1 -1
- package/README.zh.md +1 -1
- package/dist/main.js +107 -13
- package/package.json +1 -1
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.
|
|
11
|
+
**Current release: v0.3.29**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
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
|
|
121877
|
+
`[housekeeping] \u8BDD\u9898 ${tid} idle ${idleHours}h+,\u5DE5\u4F5C\u76EE\u5F55\u53EF\u6E05\u7406`
|
|
121859
121878
|
);
|
|
121860
|
-
void
|
|
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
|
|
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(
|
|
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 ${
|
|
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
|
-
|
|
121912
|
+
if (name.startsWith("_")) continue;
|
|
121913
|
+
const dirPath = pathJoin(reclaimDir, name);
|
|
121894
121914
|
let ageMs;
|
|
121895
121915
|
try {
|
|
121896
|
-
ageMs = now - (await stat(
|
|
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
|
|
121923
|
+
`[housekeeping] orphan ${name}(\u65E0 session \u8BB0\u5F55, idle ${ageHours}h+)\u2014 \u6E05\u7406`
|
|
121904
121924
|
);
|
|
121905
|
-
await
|
|
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({
|
|
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,
|