@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.
Files changed (175) hide show
  1. package/dashboard-dist/assets/index-BZ1x7x1X.js.map +1 -1
  2. package/dist/agent-farm/cli.d.ts.map +1 -1
  3. package/dist/agent-farm/cli.js +31 -0
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/attach.js +2 -2
  6. package/dist/agent-farm/commands/attach.js.map +1 -1
  7. package/dist/agent-farm/commands/send.d.ts +2 -2
  8. package/dist/agent-farm/commands/send.js +3 -3
  9. package/dist/agent-farm/commands/send.js.map +1 -1
  10. package/dist/agent-farm/commands/setup.d.ts +4 -1
  11. package/dist/agent-farm/commands/setup.d.ts.map +1 -1
  12. package/dist/agent-farm/commands/setup.js +8 -4
  13. package/dist/agent-farm/commands/setup.js.map +1 -1
  14. package/dist/agent-farm/commands/spawn-worktree.d.ts +11 -0
  15. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  16. package/dist/agent-farm/commands/spawn-worktree.js +64 -16
  17. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  18. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  19. package/dist/agent-farm/commands/status.js +26 -2
  20. package/dist/agent-farm/commands/status.js.map +1 -1
  21. package/dist/agent-farm/commands/tower-sweep-husks.d.ts +6 -0
  22. package/dist/agent-farm/commands/tower-sweep-husks.d.ts.map +1 -0
  23. package/dist/agent-farm/commands/tower-sweep-husks.js +77 -0
  24. package/dist/agent-farm/commands/tower-sweep-husks.js.map +1 -0
  25. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  26. package/dist/agent-farm/commands/tower.js +34 -0
  27. package/dist/agent-farm/commands/tower.js.map +1 -1
  28. package/dist/agent-farm/commands/whoami.d.ts +65 -0
  29. package/dist/agent-farm/commands/whoami.d.ts.map +1 -0
  30. package/dist/agent-farm/commands/whoami.js +196 -0
  31. package/dist/agent-farm/commands/whoami.js.map +1 -0
  32. package/dist/agent-farm/commands/workspace-recover.d.ts +31 -0
  33. package/dist/agent-farm/commands/workspace-recover.d.ts.map +1 -1
  34. package/dist/agent-farm/commands/workspace-recover.js +70 -30
  35. package/dist/agent-farm/commands/workspace-recover.js.map +1 -1
  36. package/dist/agent-farm/db/index.d.ts.map +1 -1
  37. package/dist/agent-farm/db/index.js +8 -2
  38. package/dist/agent-farm/db/index.js.map +1 -1
  39. package/dist/agent-farm/lib/tower-client.d.ts +1 -1
  40. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  41. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  42. package/dist/agent-farm/servers/architect-session-holder.d.ts +133 -0
  43. package/dist/agent-farm/servers/architect-session-holder.d.ts.map +1 -0
  44. package/dist/agent-farm/servers/architect-session-holder.js +268 -0
  45. package/dist/agent-farm/servers/architect-session-holder.js.map +1 -0
  46. package/dist/agent-farm/servers/process-census.d.ts +41 -0
  47. package/dist/agent-farm/servers/process-census.d.ts.map +1 -0
  48. package/dist/agent-farm/servers/process-census.js +62 -0
  49. package/dist/agent-farm/servers/process-census.js.map +1 -0
  50. package/dist/agent-farm/servers/send-buffer.d.ts.map +1 -1
  51. package/dist/agent-farm/servers/send-buffer.js +13 -0
  52. package/dist/agent-farm/servers/send-buffer.js.map +1 -1
  53. package/dist/agent-farm/servers/session-log-sweep.d.ts +93 -0
  54. package/dist/agent-farm/servers/session-log-sweep.d.ts.map +1 -0
  55. package/dist/agent-farm/servers/session-log-sweep.js +165 -0
  56. package/dist/agent-farm/servers/session-log-sweep.js.map +1 -0
  57. package/dist/agent-farm/servers/shellper-husk-sweep.d.ts +87 -0
  58. package/dist/agent-farm/servers/shellper-husk-sweep.d.ts.map +1 -0
  59. package/dist/agent-farm/servers/shellper-husk-sweep.js +129 -0
  60. package/dist/agent-farm/servers/shellper-husk-sweep.js.map +1 -0
  61. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  62. package/dist/agent-farm/servers/tower-instances.js +203 -36
  63. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  64. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  65. package/dist/agent-farm/servers/tower-routes.js +147 -4
  66. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  67. package/dist/agent-farm/servers/tower-server.js +95 -1
  68. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  69. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  70. package/dist/agent-farm/servers/tower-terminals.js +66 -8
  71. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  72. package/dist/agent-farm/servers/tower-utils.d.ts +106 -4
  73. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  74. package/dist/agent-farm/servers/tower-utils.js +155 -19
  75. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  76. package/dist/agent-farm/state.d.ts +13 -1
  77. package/dist/agent-farm/state.d.ts.map +1 -1
  78. package/dist/agent-farm/state.js +17 -2
  79. package/dist/agent-farm/state.js.map +1 -1
  80. package/dist/agent-farm/utils/claude-session-discovery.d.ts +12 -0
  81. package/dist/agent-farm/utils/claude-session-discovery.d.ts.map +1 -1
  82. package/dist/agent-farm/utils/claude-session-discovery.js +42 -4
  83. package/dist/agent-farm/utils/claude-session-discovery.js.map +1 -1
  84. package/dist/agent-farm/utils/harness.d.ts +15 -3
  85. package/dist/agent-farm/utils/harness.d.ts.map +1 -1
  86. package/dist/agent-farm/utils/harness.js +4 -1
  87. package/dist/agent-farm/utils/harness.js.map +1 -1
  88. package/dist/commands/adopt.d.ts +3 -2
  89. package/dist/commands/adopt.d.ts.map +1 -1
  90. package/dist/commands/adopt.js +7 -6
  91. package/dist/commands/adopt.js.map +1 -1
  92. package/dist/commands/consult/agy-auth-cache.d.ts +97 -0
  93. package/dist/commands/consult/agy-auth-cache.d.ts.map +1 -0
  94. package/dist/commands/consult/agy-auth-cache.js +320 -0
  95. package/dist/commands/consult/agy-auth-cache.js.map +1 -0
  96. package/dist/commands/consult/index.d.ts.map +1 -1
  97. package/dist/commands/consult/index.js +59 -0
  98. package/dist/commands/consult/index.js.map +1 -1
  99. package/dist/commands/doctor.d.ts +28 -0
  100. package/dist/commands/doctor.d.ts.map +1 -1
  101. package/dist/commands/doctor.js +187 -3
  102. package/dist/commands/doctor.js.map +1 -1
  103. package/dist/commands/init.d.ts +3 -2
  104. package/dist/commands/init.d.ts.map +1 -1
  105. package/dist/commands/init.js +7 -6
  106. package/dist/commands/init.js.map +1 -1
  107. package/dist/commands/porch/index.d.ts.map +1 -1
  108. package/dist/commands/porch/index.js +11 -8
  109. package/dist/commands/porch/index.js.map +1 -1
  110. package/dist/commands/update.d.ts +4 -4
  111. package/dist/commands/update.d.ts.map +1 -1
  112. package/dist/commands/update.js +9 -11
  113. package/dist/commands/update.js.map +1 -1
  114. package/dist/lib/config.d.ts +1 -0
  115. package/dist/lib/config.d.ts.map +1 -1
  116. package/dist/lib/config.js.map +1 -1
  117. package/dist/lib/default-branch.d.ts +1 -1
  118. package/dist/lib/default-branch.js +1 -1
  119. package/dist/lib/gitignore.d.ts +25 -2
  120. package/dist/lib/gitignore.d.ts.map +1 -1
  121. package/dist/lib/gitignore.js +70 -0
  122. package/dist/lib/gitignore.js.map +1 -1
  123. package/dist/lib/migration-backup-audit.d.ts +64 -0
  124. package/dist/lib/migration-backup-audit.d.ts.map +1 -0
  125. package/dist/lib/migration-backup-audit.js +145 -0
  126. package/dist/lib/migration-backup-audit.js.map +1 -0
  127. package/dist/lib/protocol-drift-audit.d.ts +118 -0
  128. package/dist/lib/protocol-drift-audit.d.ts.map +1 -0
  129. package/dist/lib/protocol-drift-audit.js +217 -0
  130. package/dist/lib/protocol-drift-audit.js.map +1 -0
  131. package/dist/lib/scaffold.d.ts +10 -3
  132. package/dist/lib/scaffold.d.ts.map +1 -1
  133. package/dist/lib/scaffold.js +29 -26
  134. package/dist/lib/scaffold.js.map +1 -1
  135. package/dist/terminal/pty-manager.d.ts +7 -0
  136. package/dist/terminal/pty-manager.d.ts.map +1 -1
  137. package/dist/terminal/pty-manager.js +16 -0
  138. package/dist/terminal/pty-manager.js.map +1 -1
  139. package/dist/terminal/pty-session.d.ts +25 -4
  140. package/dist/terminal/pty-session.d.ts.map +1 -1
  141. package/dist/terminal/pty-session.js +83 -11
  142. package/dist/terminal/pty-session.js.map +1 -1
  143. package/dist/terminal/session-manager.d.ts +109 -0
  144. package/dist/terminal/session-manager.d.ts.map +1 -1
  145. package/dist/terminal/session-manager.js +314 -45
  146. package/dist/terminal/session-manager.js.map +1 -1
  147. package/dist/terminal/shellper-client.d.ts +19 -4
  148. package/dist/terminal/shellper-client.d.ts.map +1 -1
  149. package/dist/terminal/shellper-client.js +70 -8
  150. package/dist/terminal/shellper-client.js.map +1 -1
  151. package/dist/terminal/shellper-process.d.ts.map +1 -1
  152. package/dist/terminal/shellper-process.js +20 -5
  153. package/dist/terminal/shellper-process.js.map +1 -1
  154. package/dist/terminal/shellper-protocol.d.ts +40 -2
  155. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  156. package/dist/terminal/shellper-protocol.js +72 -5
  157. package/dist/terminal/shellper-protocol.js.map +1 -1
  158. package/package.json +8 -8
  159. package/skeleton/.claude/skills/afx/SKILL.md +13 -0
  160. package/skeleton/.claude/skills/arch-init/SKILL.md +122 -0
  161. package/skeleton/.claude/skills/codev/SKILL.md +2 -2
  162. package/skeleton/.codex/skills/afx/SKILL.md +157 -0
  163. package/skeleton/.codex/skills/arch-init/SKILL.md +122 -0
  164. package/skeleton/.codex/skills/codev/SKILL.md +59 -0
  165. package/skeleton/.codex/skills/consult/SKILL.md +96 -0
  166. package/skeleton/.codex/skills/generate-image/SKILL.md +45 -0
  167. package/skeleton/.codex/skills/porch/SKILL.md +60 -0
  168. package/skeleton/.codex/skills/update-arch-docs/SKILL.md +132 -0
  169. package/skeleton/protocols/pir/prompts/implement.md +1 -1
  170. package/skeleton/protocols/pir/prompts/review.md +1 -1
  171. package/skeleton/protocols/pir/protocol.md +3 -3
  172. package/skeleton/resources/commands/agent-farm.md +89 -0
  173. package/skeleton/resources/commands/codev.md +5 -0
  174. package/skeleton/resources/commands/consult.md +26 -0
  175. 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,2EAA2E;AAC3E,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAE3C,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAC7C,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEjE,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC1E,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,6BAA6B,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC5E,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,gCAAgC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACvF,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,MAAM,SAAS,GAAwB,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;IACnF,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACzC,IAAI,IAAc,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,6CAA6C;QACzD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,SAAS;QAC7B,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC;QACrB,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC;QAC7B,IAAI,SAAS,CAAC,aAAa,KAAK,IAAI,IAAI,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC,aAAa,EAAE,CAAC;YAC/E,SAAS,CAAC,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC;QACzC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AA8BD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA6B;IAC5D,MAAM,MAAM,GAA2B;QACrC,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;QACV,UAAU,EAAE,CAAC;QACb,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,CAAC;KACV,CAAC;IACF,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC;QAAE,OAAO,MAAM,CAAC;IAEzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC;IAEpD,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,CAAC,oDAAoD;IACrE,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAE9C,IAAI,IAAc,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,SAAS;QAC7B,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QAEpB,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW;YAAE,SAAS;QACpD,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,aAAa,IAAI,CAAC,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,IAAI,CAAC;YACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YACxB,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,EAAE,CACR,8BAA8B,MAAM,CAAC,OAAO,kBAAkB,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE;YAC9F,KAAK,MAAM,CAAC,OAAO,UAAU;YAC7B,CAAC,MAAM,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,aAAa,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1E,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YACtD,GAAG,CACJ,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,OAAO,KAAK,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,KAAK,IAAI,IAAI,CAAC;QACd,IAAI,IAAI,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AAC/E,CAAC"}
@@ -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":"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"}
1
+ {"version":3,"file":"tower-instances.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/tower-instances.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAyB1F,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACpD,kBAAkB,EAAE,MAAM,eAAe,CAAC;IAC1C,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,0DAA0D;IAC1D,0BAA0B,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAC1E,+CAA+C;IAC/C,mBAAmB,EAAE,CACnB,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EACrD,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,EACzC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,EAC9F,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,KACvC,IAAI,CAAC;IACV,gDAAgD;IAChD,qBAAqB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,uDAAuD;IACvD,+BAA+B,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yDAAyD;IACzD,0BAA0B,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,wCAAwC;IACxC,wBAAwB,EAAE,CACxB,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KACpC,OAAO,CAAC;QAAE,SAAS,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC,CAAC;CAC9C;AA2BD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BjH;AAMD,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAEtD;AAED,sCAAsC;AACtC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAMD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAWlE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAiCjD;AAMD;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAwE9D;AAMD;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,EAAE,CAAC,CAqElH;AAyCD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAsW5H;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY7G;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA6G1H;AAMD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAoQ7B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyI/C"}