@cluesmith/codev 3.2.2 → 3.2.4
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/dashboard-dist/assets/index-BZ1x7x1X.js.map +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +31 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/attach.js +2 -2
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +2 -2
- package/dist/agent-farm/commands/send.js +3 -3
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/setup.d.ts +4 -1
- package/dist/agent-farm/commands/setup.d.ts.map +1 -1
- package/dist/agent-farm/commands/setup.js +8 -4
- package/dist/agent-farm/commands/setup.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -0
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +64 -16
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +26 -2
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/tower-sweep-husks.d.ts +6 -0
- package/dist/agent-farm/commands/tower-sweep-husks.d.ts.map +1 -0
- package/dist/agent-farm/commands/tower-sweep-husks.js +77 -0
- package/dist/agent-farm/commands/tower-sweep-husks.js.map +1 -0
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +34 -0
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/commands/whoami.d.ts +65 -0
- package/dist/agent-farm/commands/whoami.d.ts.map +1 -0
- package/dist/agent-farm/commands/whoami.js +196 -0
- package/dist/agent-farm/commands/whoami.js.map +1 -0
- package/dist/agent-farm/commands/workspace-recover.d.ts +31 -0
- package/dist/agent-farm/commands/workspace-recover.d.ts.map +1 -1
- package/dist/agent-farm/commands/workspace-recover.js +70 -30
- package/dist/agent-farm/commands/workspace-recover.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +8 -2
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts +1 -1
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/servers/architect-session-holder.d.ts +133 -0
- package/dist/agent-farm/servers/architect-session-holder.d.ts.map +1 -0
- package/dist/agent-farm/servers/architect-session-holder.js +268 -0
- package/dist/agent-farm/servers/architect-session-holder.js.map +1 -0
- package/dist/agent-farm/servers/process-census.d.ts +41 -0
- package/dist/agent-farm/servers/process-census.d.ts.map +1 -0
- package/dist/agent-farm/servers/process-census.js +62 -0
- package/dist/agent-farm/servers/process-census.js.map +1 -0
- package/dist/agent-farm/servers/send-buffer.d.ts.map +1 -1
- package/dist/agent-farm/servers/send-buffer.js +13 -0
- package/dist/agent-farm/servers/send-buffer.js.map +1 -1
- package/dist/agent-farm/servers/session-log-sweep.d.ts +93 -0
- package/dist/agent-farm/servers/session-log-sweep.d.ts.map +1 -0
- package/dist/agent-farm/servers/session-log-sweep.js +165 -0
- package/dist/agent-farm/servers/session-log-sweep.js.map +1 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.d.ts +87 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.d.ts.map +1 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.js +129 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.js.map +1 -0
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +203 -36
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +147 -4
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +95 -1
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +66 -8
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +106 -4
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +155 -19
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/state.d.ts +13 -1
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +17 -2
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/utils/claude-session-discovery.d.ts +12 -0
- package/dist/agent-farm/utils/claude-session-discovery.d.ts.map +1 -1
- package/dist/agent-farm/utils/claude-session-discovery.js +42 -4
- package/dist/agent-farm/utils/claude-session-discovery.js.map +1 -1
- package/dist/agent-farm/utils/harness.d.ts +15 -3
- package/dist/agent-farm/utils/harness.d.ts.map +1 -1
- package/dist/agent-farm/utils/harness.js +4 -1
- package/dist/agent-farm/utils/harness.js.map +1 -1
- package/dist/commands/adopt.d.ts +3 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +7 -6
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/agy-auth-cache.d.ts +97 -0
- package/dist/commands/consult/agy-auth-cache.d.ts.map +1 -0
- package/dist/commands/consult/agy-auth-cache.js +320 -0
- package/dist/commands/consult/agy-auth-cache.js.map +1 -0
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +59 -0
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +187 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +11 -8
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/update.d.ts +4 -4
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/default-branch.d.ts +1 -1
- package/dist/lib/default-branch.js +1 -1
- package/dist/lib/gitignore.d.ts +25 -2
- package/dist/lib/gitignore.d.ts.map +1 -1
- package/dist/lib/gitignore.js +70 -0
- package/dist/lib/gitignore.js.map +1 -1
- package/dist/lib/migration-backup-audit.d.ts +64 -0
- package/dist/lib/migration-backup-audit.d.ts.map +1 -0
- package/dist/lib/migration-backup-audit.js +145 -0
- package/dist/lib/migration-backup-audit.js.map +1 -0
- package/dist/lib/protocol-drift-audit.d.ts +118 -0
- package/dist/lib/protocol-drift-audit.d.ts.map +1 -0
- package/dist/lib/protocol-drift-audit.js +217 -0
- package/dist/lib/protocol-drift-audit.js.map +1 -0
- package/dist/lib/scaffold.d.ts +10 -3
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +29 -26
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +7 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +16 -0
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +25 -4
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +83 -11
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +109 -0
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +314 -45
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/shellper-client.d.ts +19 -4
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +70 -8
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +20 -5
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +40 -2
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js +72 -5
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/package.json +8 -8
- package/skeleton/.claude/skills/afx/SKILL.md +13 -0
- package/skeleton/.claude/skills/arch-init/SKILL.md +122 -0
- package/skeleton/.claude/skills/codev/SKILL.md +2 -2
- package/skeleton/.codex/skills/afx/SKILL.md +157 -0
- package/skeleton/.codex/skills/arch-init/SKILL.md +122 -0
- package/skeleton/.codex/skills/codev/SKILL.md +59 -0
- package/skeleton/.codex/skills/consult/SKILL.md +96 -0
- package/skeleton/.codex/skills/generate-image/SKILL.md +45 -0
- package/skeleton/.codex/skills/porch/SKILL.md +60 -0
- package/skeleton/.codex/skills/update-arch-docs/SKILL.md +132 -0
- package/skeleton/protocols/pir/prompts/implement.md +1 -1
- package/skeleton/protocols/pir/prompts/review.md +1 -1
- package/skeleton/protocols/pir/protocol.md +3 -3
- package/skeleton/resources/commands/agent-farm.md +89 -0
- package/skeleton/resources/commands/codev.md +5 -0
- package/skeleton/resources/commands/consult.md +26 -0
- package/skeleton/roles/architect.md +11 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1238: retention for PTY session logs.
|
|
3
|
+
*
|
|
4
|
+
* `PtySession` already bounds a *single* session's log (50 MB, one `.1`
|
|
5
|
+
* rotation — see `../../terminal/pty-session.ts`), but nothing has ever
|
|
6
|
+
* deleted the log of a session that ended. Every terminal Tower has ever
|
|
7
|
+
* opened leaves a file behind forever: an audited machine reached 29,728 files
|
|
8
|
+
* / 19 GB in `~/.agent-farm/logs`, 97% of it untouched for over a month.
|
|
9
|
+
*
|
|
10
|
+
* This module adds the missing cross-file policy: a log is deleted once its
|
|
11
|
+
* last write is older than the retention window. mtime is the retention key
|
|
12
|
+
* because the last byte written to a session log is, in effect, the moment
|
|
13
|
+
* that session stopped producing output — the "session ended" signal, without
|
|
14
|
+
* needing to join against DB session state (which is itself pruned, so dead
|
|
15
|
+
* sessions leave no row to join against).
|
|
16
|
+
*
|
|
17
|
+
* Live sessions are excluded by id regardless of mtime. A session log's fd is
|
|
18
|
+
* held open for the session's whole life, so unlinking it under a live writer
|
|
19
|
+
* would silently redirect every subsequent write into an unlinked inode —
|
|
20
|
+
* losing the log of the one session someone might actually want to read. A
|
|
21
|
+
* 30-day-idle live session is vanishingly unlikely, but the guard is cheap.
|
|
22
|
+
*
|
|
23
|
+
* Wired in `./tower-server.ts` on two triggers (Tower startup, and a daily
|
|
24
|
+
* in-process timer), mirroring the husk sweep in `./shellper-husk-sweep.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_LOG_RETENTION_DAYS = 30;
|
|
27
|
+
export declare const DEFAULT_LOG_SWEEP_INTERVAL_MS: number;
|
|
28
|
+
export declare const MS_PER_DAY: number;
|
|
29
|
+
/**
|
|
30
|
+
* Retention window in days (env `AGENT_FARM_LOG_RETENTION_DAYS`, default 30).
|
|
31
|
+
* `0` disables the sweep entirely — an explicit "keep everything" opt-out for
|
|
32
|
+
* anyone who treats these logs as an audit trail.
|
|
33
|
+
*
|
|
34
|
+
* NaN-checked rather than `parsed || default` because `0` is a meaningful
|
|
35
|
+
* override and is falsy in JS (same reasoning as `resolveHuskGraceMs`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveLogRetentionDays(env?: NodeJS.ProcessEnv): number;
|
|
38
|
+
/**
|
|
39
|
+
* Sweep cadence (env `AGENT_FARM_LOG_SWEEP_INTERVAL_MS`, default 24h). Floored
|
|
40
|
+
* at 1s so a bad value can't spin the event loop; tests shorten it deliberately.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveLogSweepIntervalMs(env?: NodeJS.ProcessEnv): number;
|
|
43
|
+
export interface SessionLogFootprint {
|
|
44
|
+
/** Number of session log files (including `.log.1` rotations). */
|
|
45
|
+
files: number;
|
|
46
|
+
/** Total bytes on disk. */
|
|
47
|
+
bytes: number;
|
|
48
|
+
/** mtime of the oldest log, or null when there are none. */
|
|
49
|
+
oldestMtimeMs: number | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Total footprint of the session log directory. Read-only — used by
|
|
53
|
+
* `codev doctor` to surface the number the issue was filed about, and by the
|
|
54
|
+
* sweep's own logging. A missing directory is a healthy zero, not an error.
|
|
55
|
+
*/
|
|
56
|
+
export declare function measureSessionLogs(logDir: string): SessionLogFootprint;
|
|
57
|
+
export interface SweepSessionLogsOptions {
|
|
58
|
+
/** The session log directory, i.e. `~/.agent-farm/logs`. */
|
|
59
|
+
logDir: string;
|
|
60
|
+
/** Age at which a log becomes eligible for deletion. `0` disables the sweep. */
|
|
61
|
+
retentionMs: number;
|
|
62
|
+
/**
|
|
63
|
+
* Ids of sessions that are alive right now. Their logs are never deleted,
|
|
64
|
+
* however old the mtime looks, because their fd is still open.
|
|
65
|
+
*/
|
|
66
|
+
activeSessionIds?: Iterable<string>;
|
|
67
|
+
/** Seam, defaults to `Date.now()`. */
|
|
68
|
+
now?: number;
|
|
69
|
+
log?: (msg: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface SweepSessionLogsResult {
|
|
72
|
+
/** Session log files examined. */
|
|
73
|
+
scanned: number;
|
|
74
|
+
/** Files unlinked. */
|
|
75
|
+
deleted: number;
|
|
76
|
+
/** Bytes reclaimed (sum of the deleted files' sizes). */
|
|
77
|
+
bytesFreed: number;
|
|
78
|
+
/** Aged-out files kept because their session is still live. */
|
|
79
|
+
skippedActive: number;
|
|
80
|
+
/** Files that were eligible but could not be unlinked. */
|
|
81
|
+
failed: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Delete session logs whose last write is older than `retentionMs`.
|
|
85
|
+
*
|
|
86
|
+
* Never throws: a log directory that can't be read, or an individual file that
|
|
87
|
+
* can't be stat'd or unlinked, is counted and skipped. Log retention is
|
|
88
|
+
* janitorial — it must not be able to take Tower's startup path down with it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function sweepSessionLogs(opts: SweepSessionLogsOptions): SweepSessionLogsResult;
|
|
91
|
+
/** Human-readable byte count for log lines and `codev doctor` output. */
|
|
92
|
+
export declare function formatBytes(bytes: number): string;
|
|
93
|
+
//# sourceMappingURL=session-log-sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-log-sweep.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/session-log-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAC7C,eAAO,MAAM,6BAA6B,QAAsB,CAAC;AAEjE,eAAO,MAAM,UAAU,QAAsB,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGpF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGtF;AAED,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAyBtE;AAED,MAAM,WAAW,uBAAuB;IACtC,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACrC,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,sBAAsB,CA2DtF;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUjD"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1238: retention for PTY session logs.
|
|
3
|
+
*
|
|
4
|
+
* `PtySession` already bounds a *single* session's log (50 MB, one `.1`
|
|
5
|
+
* rotation — see `../../terminal/pty-session.ts`), but nothing has ever
|
|
6
|
+
* deleted the log of a session that ended. Every terminal Tower has ever
|
|
7
|
+
* opened leaves a file behind forever: an audited machine reached 29,728 files
|
|
8
|
+
* / 19 GB in `~/.agent-farm/logs`, 97% of it untouched for over a month.
|
|
9
|
+
*
|
|
10
|
+
* This module adds the missing cross-file policy: a log is deleted once its
|
|
11
|
+
* last write is older than the retention window. mtime is the retention key
|
|
12
|
+
* because the last byte written to a session log is, in effect, the moment
|
|
13
|
+
* that session stopped producing output — the "session ended" signal, without
|
|
14
|
+
* needing to join against DB session state (which is itself pruned, so dead
|
|
15
|
+
* sessions leave no row to join against).
|
|
16
|
+
*
|
|
17
|
+
* Live sessions are excluded by id regardless of mtime. A session log's fd is
|
|
18
|
+
* held open for the session's whole life, so unlinking it under a live writer
|
|
19
|
+
* would silently redirect every subsequent write into an unlinked inode —
|
|
20
|
+
* losing the log of the one session someone might actually want to read. A
|
|
21
|
+
* 30-day-idle live session is vanishingly unlikely, but the guard is cheap.
|
|
22
|
+
*
|
|
23
|
+
* Wired in `./tower-server.ts` on two triggers (Tower startup, and a daily
|
|
24
|
+
* in-process timer), mirroring the husk sweep in `./shellper-husk-sweep.ts`.
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
/** `<session-uuid>.log` and its single rotation `<session-uuid>.log.1`. */
|
|
29
|
+
const SESSION_LOG_RE = /^(.+)\.log(\.1)?$/;
|
|
30
|
+
export const DEFAULT_LOG_RETENTION_DAYS = 30;
|
|
31
|
+
export const DEFAULT_LOG_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
export const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
33
|
+
/**
|
|
34
|
+
* Retention window in days (env `AGENT_FARM_LOG_RETENTION_DAYS`, default 30).
|
|
35
|
+
* `0` disables the sweep entirely — an explicit "keep everything" opt-out for
|
|
36
|
+
* anyone who treats these logs as an audit trail.
|
|
37
|
+
*
|
|
38
|
+
* NaN-checked rather than `parsed || default` because `0` is a meaningful
|
|
39
|
+
* override and is falsy in JS (same reasoning as `resolveHuskGraceMs`).
|
|
40
|
+
*/
|
|
41
|
+
export function resolveLogRetentionDays(env = process.env) {
|
|
42
|
+
const parsed = parseInt(env.AGENT_FARM_LOG_RETENTION_DAYS ?? '', 10);
|
|
43
|
+
return Math.max(Number.isNaN(parsed) ? DEFAULT_LOG_RETENTION_DAYS : parsed, 0);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sweep cadence (env `AGENT_FARM_LOG_SWEEP_INTERVAL_MS`, default 24h). Floored
|
|
47
|
+
* at 1s so a bad value can't spin the event loop; tests shorten it deliberately.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveLogSweepIntervalMs(env = process.env) {
|
|
50
|
+
const parsed = parseInt(env.AGENT_FARM_LOG_SWEEP_INTERVAL_MS ?? '', 10);
|
|
51
|
+
return Math.max(Number.isNaN(parsed) ? DEFAULT_LOG_SWEEP_INTERVAL_MS : parsed, 1000);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Total footprint of the session log directory. Read-only — used by
|
|
55
|
+
* `codev doctor` to surface the number the issue was filed about, and by the
|
|
56
|
+
* sweep's own logging. A missing directory is a healthy zero, not an error.
|
|
57
|
+
*/
|
|
58
|
+
export function measureSessionLogs(logDir) {
|
|
59
|
+
const footprint = { files: 0, bytes: 0, oldestMtimeMs: null };
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = fs.readdirSync(logDir);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return footprint;
|
|
66
|
+
}
|
|
67
|
+
for (const name of entries) {
|
|
68
|
+
if (!SESSION_LOG_RE.test(name))
|
|
69
|
+
continue;
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = fs.statSync(path.join(logDir, name));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue; // vanished mid-scan — nothing to account for
|
|
76
|
+
}
|
|
77
|
+
if (!stat.isFile())
|
|
78
|
+
continue;
|
|
79
|
+
footprint.files += 1;
|
|
80
|
+
footprint.bytes += stat.size;
|
|
81
|
+
if (footprint.oldestMtimeMs === null || stat.mtimeMs < footprint.oldestMtimeMs) {
|
|
82
|
+
footprint.oldestMtimeMs = stat.mtimeMs;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return footprint;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Delete session logs whose last write is older than `retentionMs`.
|
|
89
|
+
*
|
|
90
|
+
* Never throws: a log directory that can't be read, or an individual file that
|
|
91
|
+
* can't be stat'd or unlinked, is counted and skipped. Log retention is
|
|
92
|
+
* janitorial — it must not be able to take Tower's startup path down with it.
|
|
93
|
+
*/
|
|
94
|
+
export function sweepSessionLogs(opts) {
|
|
95
|
+
const result = {
|
|
96
|
+
scanned: 0,
|
|
97
|
+
deleted: 0,
|
|
98
|
+
bytesFreed: 0,
|
|
99
|
+
skippedActive: 0,
|
|
100
|
+
failed: 0,
|
|
101
|
+
};
|
|
102
|
+
if (opts.retentionMs <= 0)
|
|
103
|
+
return result;
|
|
104
|
+
const now = opts.now ?? Date.now();
|
|
105
|
+
const active = new Set(opts.activeSessionIds ?? []);
|
|
106
|
+
let entries;
|
|
107
|
+
try {
|
|
108
|
+
entries = fs.readdirSync(opts.logDir);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return result; // no log dir yet (fresh install) — nothing to sweep
|
|
112
|
+
}
|
|
113
|
+
for (const name of entries) {
|
|
114
|
+
const match = SESSION_LOG_RE.exec(name);
|
|
115
|
+
if (!match)
|
|
116
|
+
continue;
|
|
117
|
+
const fullPath = path.join(opts.logDir, name);
|
|
118
|
+
let stat;
|
|
119
|
+
try {
|
|
120
|
+
stat = fs.statSync(fullPath);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!stat.isFile())
|
|
126
|
+
continue;
|
|
127
|
+
result.scanned += 1;
|
|
128
|
+
if (now - stat.mtimeMs < opts.retentionMs)
|
|
129
|
+
continue;
|
|
130
|
+
if (active.has(match[1])) {
|
|
131
|
+
result.skippedActive += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(fullPath);
|
|
136
|
+
result.deleted += 1;
|
|
137
|
+
result.bytesFreed += stat.size;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
result.failed += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (result.deleted > 0 || result.failed > 0) {
|
|
144
|
+
opts.log?.(`Session log sweep: deleted ${result.deleted} log(s), freed ${formatBytes(result.bytesFreed)}` +
|
|
145
|
+
` (${result.scanned} scanned` +
|
|
146
|
+
(result.skippedActive > 0 ? `, ${result.skippedActive} kept as live` : '') +
|
|
147
|
+
(result.failed > 0 ? `, ${result.failed} failed` : '') +
|
|
148
|
+
')');
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
/** Human-readable byte count for log lines and `codev doctor` output. */
|
|
153
|
+
export function formatBytes(bytes) {
|
|
154
|
+
if (bytes < 1024)
|
|
155
|
+
return `${bytes} B`;
|
|
156
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
157
|
+
let value = bytes / 1024;
|
|
158
|
+
let unit = 0;
|
|
159
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
160
|
+
value /= 1024;
|
|
161
|
+
unit += 1;
|
|
162
|
+
}
|
|
163
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=session-log-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-log-sweep.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/session-log-sweep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1227: a stricter, second-pass reap for shellper "husks" — a shellper
|
|
3
|
+
* whose PTY child has exited (deliberate lingering behavior from #905/#1198 so
|
|
4
|
+
* a reconnect can replay history) but which `killOrphanedShellpers`
|
|
5
|
+
* (`../../terminal/session-manager.ts`) can never reap, because that function
|
|
6
|
+
* unconditionally protects any shellper whose Unix socket still responds.
|
|
7
|
+
*
|
|
8
|
+
* A husk still answers its socket, so it survives `killOrphanedShellpers`
|
|
9
|
+
* forever. The predicate here is stricter and orthogonal: a shellper is a husk
|
|
10
|
+
* only when it is simultaneously
|
|
11
|
+
* - unregistered — not a live PID in `terminal_sessions.shellper_pid`
|
|
12
|
+
* - childless — no process in the table has it as a parent (OS-level
|
|
13
|
+
* signal; there is no wire-protocol "has-child" frame)
|
|
14
|
+
* - aged — older than `graceMs`, which absorbs any legitimate
|
|
15
|
+
* mid-respawn / mid-adoption transient (seconds), so the
|
|
16
|
+
* three conditions together can only ever be true for a
|
|
17
|
+
* genuine husk (per the issue: "unregistered and
|
|
18
|
+
* childless" is a terminal state — a shellper that lost
|
|
19
|
+
* its registration can never receive another SPAWN).
|
|
20
|
+
*
|
|
21
|
+
* The actual kill is delegated to `reapShellpers` (`./architect-session-holder.js`,
|
|
22
|
+
* SIGTERM → poll → SIGKILL → confirm-death) rather than reimplemented here.
|
|
23
|
+
*/
|
|
24
|
+
import type Database from 'better-sqlite3';
|
|
25
|
+
import { type ProcessCensusEntry } from './process-census.js';
|
|
26
|
+
export declare const SHELLPER_MARKER = "shellper-main.js";
|
|
27
|
+
/**
|
|
28
|
+
* The set of shellper PIDs Tower currently believes are registered, per
|
|
29
|
+
* `terminal_sessions` — validated against each PID's actual process start
|
|
30
|
+
* time to guard against PID reuse (a stale row whose PID was reused by an
|
|
31
|
+
* unrelated process does not count as registered).
|
|
32
|
+
*/
|
|
33
|
+
export declare function computeRegisteredShellperPids(db: Database.Database, getStartTime?: (pid: number) => Promise<number | null>): Promise<Set<number>>;
|
|
34
|
+
export interface FindHuskShellpersOptions {
|
|
35
|
+
/** This Tower instance's socket directory — scopes the sweep so another
|
|
36
|
+
* instance's (or another user's) legitimate shellpers are never touched. */
|
|
37
|
+
socketDir: string;
|
|
38
|
+
registeredShellperPids: Set<number>;
|
|
39
|
+
graceMs: number;
|
|
40
|
+
/**
|
|
41
|
+
* Seam, defaults to `listProcessCensus`. Accepts a plain array too (not just
|
|
42
|
+
* a function returning one) so a caller that already took a snapshot for
|
|
43
|
+
* another purpose (e.g. RSS display) can pass it through and avoid a second
|
|
44
|
+
* `ps` scan — see `handleHuskPreview` in tower-routes.ts.
|
|
45
|
+
*/
|
|
46
|
+
census?: () => ProcessCensusEntry[] | Promise<ProcessCensusEntry[]>;
|
|
47
|
+
/** Seam, defaults to `Date.now()`. */
|
|
48
|
+
now?: number;
|
|
49
|
+
/** Seam, defaults to `getProcessStartTime`. */
|
|
50
|
+
getStartTime?: (pid: number) => Promise<number | null>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns the PIDs of shellper-main.js processes, scoped to `socketDir`, that
|
|
54
|
+
* are unregistered AND childless AND older than `graceMs`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function findHuskShellpers(opts: FindHuskShellpersOptions): Promise<number[]>;
|
|
57
|
+
/**
|
|
58
|
+
* The husk grace period (env `SHELLPER_HUSK_GRACE_MS`, default 1 hour), shared
|
|
59
|
+
* by every trigger (startup, periodic, on-demand preview/apply) so a preview
|
|
60
|
+
* always predicts what the periodic sweep would actually do.
|
|
61
|
+
*
|
|
62
|
+
* NaN-checked rather than `parsed || default`: `0` is a legitimate override
|
|
63
|
+
* (tests want an immediate-eligibility grace period) and `0` is falsy in JS,
|
|
64
|
+
* so `parsed || default` would silently discard a deliberate zero.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveHuskGraceMs(env?: NodeJS.ProcessEnv): number;
|
|
67
|
+
export interface SweepShellperHusksOptions {
|
|
68
|
+
socketDir: string;
|
|
69
|
+
db: Database.Database;
|
|
70
|
+
graceMs: number;
|
|
71
|
+
log?: (msg: string) => void;
|
|
72
|
+
census?: () => ProcessCensusEntry[] | Promise<ProcessCensusEntry[]>;
|
|
73
|
+
now?: number;
|
|
74
|
+
getStartTime?: (pid: number) => Promise<number | null>;
|
|
75
|
+
/** Seam, defaults to `reapShellpers`. */
|
|
76
|
+
reap?: (pids: number[]) => Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute the registered-shellper set from the DB, find husk candidates, and
|
|
80
|
+
* reap them. Returns the swept PIDs so callers (startup log, periodic-timer
|
|
81
|
+
* log, on-demand CLI) can report what happened.
|
|
82
|
+
*/
|
|
83
|
+
export declare function sweepShellperHusks(opts: SweepShellperHusksOptions): Promise<{
|
|
84
|
+
swept: number;
|
|
85
|
+
pids: number[];
|
|
86
|
+
}>;
|
|
87
|
+
//# sourceMappingURL=shellper-husk-sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellper-husk-sweep.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/shellper-husk-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAIjF,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAgBlD;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,YAAY,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAuB,GAC1E,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAmBtB;AAED,MAAM,WAAW,wBAAwB;IACvC;gFAC4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,kBAAkB,EAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IACpE,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACxD;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAyBzF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG/E;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,kBAAkB,EAAE,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvD,yCAAyC;IACzC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAmB5C"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1227: a stricter, second-pass reap for shellper "husks" — a shellper
|
|
3
|
+
* whose PTY child has exited (deliberate lingering behavior from #905/#1198 so
|
|
4
|
+
* a reconnect can replay history) but which `killOrphanedShellpers`
|
|
5
|
+
* (`../../terminal/session-manager.ts`) can never reap, because that function
|
|
6
|
+
* unconditionally protects any shellper whose Unix socket still responds.
|
|
7
|
+
*
|
|
8
|
+
* A husk still answers its socket, so it survives `killOrphanedShellpers`
|
|
9
|
+
* forever. The predicate here is stricter and orthogonal: a shellper is a husk
|
|
10
|
+
* only when it is simultaneously
|
|
11
|
+
* - unregistered — not a live PID in `terminal_sessions.shellper_pid`
|
|
12
|
+
* - childless — no process in the table has it as a parent (OS-level
|
|
13
|
+
* signal; there is no wire-protocol "has-child" frame)
|
|
14
|
+
* - aged — older than `graceMs`, which absorbs any legitimate
|
|
15
|
+
* mid-respawn / mid-adoption transient (seconds), so the
|
|
16
|
+
* three conditions together can only ever be true for a
|
|
17
|
+
* genuine husk (per the issue: "unregistered and
|
|
18
|
+
* childless" is a terminal state — a shellper that lost
|
|
19
|
+
* its registration can never receive another SPAWN).
|
|
20
|
+
*
|
|
21
|
+
* The actual kill is delegated to `reapShellpers` (`./architect-session-holder.js`,
|
|
22
|
+
* SIGTERM → poll → SIGKILL → confirm-death) rather than reimplemented here.
|
|
23
|
+
*/
|
|
24
|
+
import { listProcessCensus } from './process-census.js';
|
|
25
|
+
import { getProcessStartTime } from '../../terminal/session-manager.js';
|
|
26
|
+
import { reapShellpers } from './architect-session-holder.js';
|
|
27
|
+
export const SHELLPER_MARKER = 'shellper-main.js';
|
|
28
|
+
/**
|
|
29
|
+
* Same tolerance `SessionManager.reconnectSession` uses
|
|
30
|
+
* (`../../terminal/session-manager.ts:471`) to detect PID reuse: a live
|
|
31
|
+
* process's actual start time must match the DB-recorded one within this
|
|
32
|
+
* window, or the DB row is stale (its PID was reused by an unrelated
|
|
33
|
+
* process) and must not count as "registered".
|
|
34
|
+
*/
|
|
35
|
+
const PID_REUSE_TOLERANCE_MS = 2000;
|
|
36
|
+
/**
|
|
37
|
+
* The set of shellper PIDs Tower currently believes are registered, per
|
|
38
|
+
* `terminal_sessions` — validated against each PID's actual process start
|
|
39
|
+
* time to guard against PID reuse (a stale row whose PID was reused by an
|
|
40
|
+
* unrelated process does not count as registered).
|
|
41
|
+
*/
|
|
42
|
+
export async function computeRegisteredShellperPids(db, getStartTime = getProcessStartTime) {
|
|
43
|
+
const rows = db
|
|
44
|
+
.prepare('SELECT shellper_pid, shellper_start_time FROM terminal_sessions WHERE shellper_pid IS NOT NULL')
|
|
45
|
+
.all();
|
|
46
|
+
const registered = new Set();
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (row.shellper_start_time === null) {
|
|
49
|
+
// Legacy row with no recorded start time — no positive signal to
|
|
50
|
+
// invalidate it, so trust the registration.
|
|
51
|
+
registered.add(row.shellper_pid);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const actual = await getStartTime(row.shellper_pid);
|
|
55
|
+
if (actual !== null && Math.abs(actual - row.shellper_start_time) <= PID_REUSE_TOLERANCE_MS) {
|
|
56
|
+
registered.add(row.shellper_pid);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return registered;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns the PIDs of shellper-main.js processes, scoped to `socketDir`, that
|
|
63
|
+
* are unregistered AND childless AND older than `graceMs`.
|
|
64
|
+
*/
|
|
65
|
+
export async function findHuskShellpers(opts) {
|
|
66
|
+
const census = await (opts.census ?? listProcessCensus)();
|
|
67
|
+
const now = opts.now ?? Date.now();
|
|
68
|
+
const getStartTime = opts.getStartTime ?? getProcessStartTime;
|
|
69
|
+
// Match the trailing-'/' scope-marker technique already proven in
|
|
70
|
+
// findShellperProcesses (../../terminal/session-manager.ts:782-793) to
|
|
71
|
+
// prevent prefix collisions (e.g. /run matching /run2).
|
|
72
|
+
const scopeMarker = opts.socketDir.endsWith('/') ? opts.socketDir : `${opts.socketDir}/`;
|
|
73
|
+
const parentPids = new Set(census.map((entry) => entry.ppid));
|
|
74
|
+
const candidates = [];
|
|
75
|
+
for (const entry of census) {
|
|
76
|
+
if (!entry.cmdline.includes(SHELLPER_MARKER))
|
|
77
|
+
continue;
|
|
78
|
+
if (!entry.cmdline.includes(scopeMarker))
|
|
79
|
+
continue;
|
|
80
|
+
if (opts.registeredShellperPids.has(entry.pid))
|
|
81
|
+
continue;
|
|
82
|
+
if (parentPids.has(entry.pid))
|
|
83
|
+
continue; // has a live child — not a husk
|
|
84
|
+
const startTime = await getStartTime(entry.pid);
|
|
85
|
+
if (startTime === null)
|
|
86
|
+
continue; // can't confirm age — safe default is to skip
|
|
87
|
+
if (now - startTime < opts.graceMs)
|
|
88
|
+
continue; // too young — race-safety window
|
|
89
|
+
candidates.push(entry.pid);
|
|
90
|
+
}
|
|
91
|
+
return candidates;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The husk grace period (env `SHELLPER_HUSK_GRACE_MS`, default 1 hour), shared
|
|
95
|
+
* by every trigger (startup, periodic, on-demand preview/apply) so a preview
|
|
96
|
+
* always predicts what the periodic sweep would actually do.
|
|
97
|
+
*
|
|
98
|
+
* NaN-checked rather than `parsed || default`: `0` is a legitimate override
|
|
99
|
+
* (tests want an immediate-eligibility grace period) and `0` is falsy in JS,
|
|
100
|
+
* so `parsed || default` would silently discard a deliberate zero.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveHuskGraceMs(env = process.env) {
|
|
103
|
+
const parsed = parseInt(env.SHELLPER_HUSK_GRACE_MS || '3600000', 10);
|
|
104
|
+
return Math.max(Number.isNaN(parsed) ? 3600000 : parsed, 0);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Compute the registered-shellper set from the DB, find husk candidates, and
|
|
108
|
+
* reap them. Returns the swept PIDs so callers (startup log, periodic-timer
|
|
109
|
+
* log, on-demand CLI) can report what happened.
|
|
110
|
+
*/
|
|
111
|
+
export async function sweepShellperHusks(opts) {
|
|
112
|
+
const getStartTime = opts.getStartTime ?? getProcessStartTime;
|
|
113
|
+
const registeredShellperPids = await computeRegisteredShellperPids(opts.db, getStartTime);
|
|
114
|
+
const pids = await findHuskShellpers({
|
|
115
|
+
socketDir: opts.socketDir,
|
|
116
|
+
registeredShellperPids,
|
|
117
|
+
graceMs: opts.graceMs,
|
|
118
|
+
census: opts.census,
|
|
119
|
+
now: opts.now,
|
|
120
|
+
getStartTime,
|
|
121
|
+
});
|
|
122
|
+
if (pids.length > 0) {
|
|
123
|
+
opts.log?.(`Sweeping ${pids.length} husk shellper process(es): ${pids.join(', ')}`);
|
|
124
|
+
const reap = opts.reap ?? reapShellpers;
|
|
125
|
+
await reap(pids);
|
|
126
|
+
}
|
|
127
|
+
return { swept: pids.length, pids };
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=shellper-husk-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellper-husk-sweep.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/shellper-husk-sweep.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tower-instances.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-instances.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"tower-instances.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-instances.ts"],"names":[],"mappings":"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"}
|