@cluesmith/codev 3.3.2 → 3.3.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 (257) hide show
  1. package/dashboard-dist/assets/{index-D3XaKpZW.js → index-DZf6pNPE.js} +41 -41
  2. package/dashboard-dist/assets/index-DZf6pNPE.js.map +1 -0
  3. package/dashboard-dist/assets/index-DnYLy5C_.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +48 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/ide.d.ts +20 -0
  9. package/dist/agent-farm/commands/ide.d.ts.map +1 -0
  10. package/dist/agent-farm/commands/ide.js +138 -0
  11. package/dist/agent-farm/commands/ide.js.map +1 -0
  12. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/inbox.js +79 -10
  14. package/dist/agent-farm/commands/inbox.js.map +1 -1
  15. package/dist/agent-farm/commands/interrupt.d.ts +4 -1
  16. package/dist/agent-farm/commands/interrupt.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/interrupt.js +6 -3
  18. package/dist/agent-farm/commands/interrupt.js.map +1 -1
  19. package/dist/agent-farm/commands/reset.d.ts +4 -1
  20. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/reset.js +6 -3
  22. package/dist/agent-farm/commands/reset.js.map +1 -1
  23. package/dist/agent-farm/commands/send.d.ts +28 -0
  24. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  25. package/dist/agent-farm/commands/send.js +79 -6
  26. package/dist/agent-farm/commands/send.js.map +1 -1
  27. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  28. package/dist/agent-farm/commands/spawn-worktree.js +16 -7
  29. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  30. package/dist/agent-farm/commands/tower.d.ts +21 -9
  31. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/tower.js +90 -30
  33. package/dist/agent-farm/commands/tower.js.map +1 -1
  34. package/dist/agent-farm/db/index.d.ts +3 -1
  35. package/dist/agent-farm/db/index.d.ts.map +1 -1
  36. package/dist/agent-farm/db/index.js +8 -443
  37. package/dist/agent-farm/db/index.js.map +1 -1
  38. package/dist/agent-farm/db/mailbox.d.ts +47 -7
  39. package/dist/agent-farm/db/mailbox.d.ts.map +1 -1
  40. package/dist/agent-farm/db/mailbox.js +56 -12
  41. package/dist/agent-farm/db/mailbox.js.map +1 -1
  42. package/dist/agent-farm/db/migrations.d.ts +57 -0
  43. package/dist/agent-farm/db/migrations.d.ts.map +1 -0
  44. package/dist/agent-farm/db/migrations.js +530 -0
  45. package/dist/agent-farm/db/migrations.js.map +1 -0
  46. package/dist/agent-farm/db/schema.d.ts +1 -1
  47. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  48. package/dist/agent-farm/db/schema.js +1 -0
  49. package/dist/agent-farm/db/schema.js.map +1 -1
  50. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  51. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  52. package/dist/agent-farm/db/tower-owner.js +369 -0
  53. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  54. package/dist/agent-farm/db/types.d.ts +47 -0
  55. package/dist/agent-farm/db/types.d.ts.map +1 -1
  56. package/dist/agent-farm/db/types.js.map +1 -1
  57. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  58. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  59. package/dist/agent-farm/lib/ide-record.js +118 -0
  60. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  61. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  62. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  63. package/dist/agent-farm/lib/tunnel-client.js +96 -19
  64. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  65. package/dist/agent-farm/servers/cron-delivery.d.ts +7 -1
  66. package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -1
  67. package/dist/agent-farm/servers/cron-delivery.js +2 -1
  68. package/dist/agent-farm/servers/cron-delivery.js.map +1 -1
  69. package/dist/agent-farm/servers/gate-profiles.d.ts +44 -2
  70. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/gate-profiles.js +79 -6
  72. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  73. package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
  74. package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
  75. package/dist/agent-farm/servers/ide-forward.js +274 -0
  76. package/dist/agent-farm/servers/ide-forward.js.map +1 -0
  77. package/dist/agent-farm/servers/ide-server.d.ts +68 -0
  78. package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
  79. package/dist/agent-farm/servers/ide-server.js +281 -0
  80. package/dist/agent-farm/servers/ide-server.js.map +1 -0
  81. package/dist/agent-farm/servers/mailbox-delivery.d.ts +296 -12
  82. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  83. package/dist/agent-farm/servers/mailbox-delivery.js +566 -69
  84. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  85. package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -19
  86. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/mailbox-wiring.js +204 -37
  88. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  89. package/dist/agent-farm/servers/message-write.d.ts +138 -11
  90. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  91. package/dist/agent-farm/servers/message-write.js +144 -35
  92. package/dist/agent-farm/servers/message-write.js.map +1 -1
  93. package/dist/agent-farm/servers/overview.d.ts +28 -12
  94. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/overview.js +130 -79
  96. package/dist/agent-farm/servers/overview.js.map +1 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts +77 -1
  98. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  99. package/dist/agent-farm/servers/render-gate.js +174 -25
  100. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  101. package/dist/agent-farm/servers/session-submit.d.ts +7 -4
  102. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  103. package/dist/agent-farm/servers/session-submit.js +7 -4
  104. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  105. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  106. package/dist/agent-farm/servers/tower-cron.js +7 -3
  107. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  108. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  109. package/dist/agent-farm/servers/tower-instances.js +17 -10
  110. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  111. package/dist/agent-farm/servers/tower-routes.d.ts +32 -0
  112. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  113. package/dist/agent-farm/servers/tower-routes.js +186 -26
  114. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  115. package/dist/agent-farm/servers/tower-server.js +79 -9
  116. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  117. package/dist/agent-farm/servers/tower-terminals.d.ts +21 -0
  118. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  119. package/dist/agent-farm/servers/tower-terminals.js +106 -13
  120. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  121. package/dist/agent-farm/servers/tower-utils.d.ts +35 -0
  122. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  123. package/dist/agent-farm/servers/tower-utils.js +39 -0
  124. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  125. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  126. package/dist/agent-farm/servers/tower-websocket.js +26 -2
  127. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  128. package/dist/agent-farm/utils/message-format.d.ts +78 -2
  129. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  130. package/dist/agent-farm/utils/message-format.js +107 -11
  131. package/dist/agent-farm/utils/message-format.js.map +1 -1
  132. package/dist/agent-farm/utils/port.d.ts +22 -0
  133. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  134. package/dist/agent-farm/utils/port.js +35 -0
  135. package/dist/agent-farm/utils/port.js.map +1 -0
  136. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  137. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  138. package/dist/agent-farm/utils/server-utils.js +36 -0
  139. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  140. package/dist/cli.d.ts.map +1 -1
  141. package/dist/cli.js +3 -1
  142. package/dist/cli.js.map +1 -1
  143. package/dist/commands/consult/index.d.ts.map +1 -1
  144. package/dist/commands/consult/index.js +61 -1
  145. package/dist/commands/consult/index.js.map +1 -1
  146. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  147. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  148. package/dist/commands/consult/tree-tripwire.js +356 -0
  149. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  150. package/dist/commands/doctor.d.ts +69 -0
  151. package/dist/commands/doctor.d.ts.map +1 -1
  152. package/dist/commands/doctor.js +165 -2
  153. package/dist/commands/doctor.js.map +1 -1
  154. package/dist/commands/generate-image.d.ts +25 -1
  155. package/dist/commands/generate-image.d.ts.map +1 -1
  156. package/dist/commands/generate-image.js +410 -2
  157. package/dist/commands/generate-image.js.map +1 -1
  158. package/dist/commands/porch/prompts.d.ts.map +1 -1
  159. package/dist/commands/porch/prompts.js +18 -2
  160. package/dist/commands/porch/prompts.js.map +1 -1
  161. package/dist/lib/agent-env.d.ts +86 -0
  162. package/dist/lib/agent-env.d.ts.map +1 -0
  163. package/dist/lib/agent-env.js +126 -0
  164. package/dist/lib/agent-env.js.map +1 -0
  165. package/dist/lib/forge-contracts.d.ts +76 -5
  166. package/dist/lib/forge-contracts.d.ts.map +1 -1
  167. package/dist/lib/forge-contracts.js.map +1 -1
  168. package/dist/lib/forge-rate-limit.d.ts +45 -0
  169. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  170. package/dist/lib/forge-rate-limit.js +89 -0
  171. package/dist/lib/forge-rate-limit.js.map +1 -0
  172. package/dist/lib/forge.d.ts +29 -1
  173. package/dist/lib/forge.d.ts.map +1 -1
  174. package/dist/lib/forge.js +96 -21
  175. package/dist/lib/forge.js.map +1 -1
  176. package/dist/lib/github.d.ts +13 -1
  177. package/dist/lib/github.d.ts.map +1 -1
  178. package/dist/lib/github.js +58 -15
  179. package/dist/lib/github.js.map +1 -1
  180. package/dist/lib/test-env.d.ts +17 -0
  181. package/dist/lib/test-env.d.ts.map +1 -1
  182. package/dist/lib/test-env.js +20 -0
  183. package/dist/lib/test-env.js.map +1 -1
  184. package/dist/terminal/pty-manager.d.ts +10 -1
  185. package/dist/terminal/pty-manager.d.ts.map +1 -1
  186. package/dist/terminal/pty-manager.js +38 -9
  187. package/dist/terminal/pty-manager.js.map +1 -1
  188. package/dist/terminal/pty-session.d.ts +174 -8
  189. package/dist/terminal/pty-session.d.ts.map +1 -1
  190. package/dist/terminal/pty-session.js +299 -25
  191. package/dist/terminal/pty-session.js.map +1 -1
  192. package/dist/terminal/session-manager.d.ts +23 -2
  193. package/dist/terminal/session-manager.d.ts.map +1 -1
  194. package/dist/terminal/session-manager.js +115 -36
  195. package/dist/terminal/session-manager.js.map +1 -1
  196. package/dist/terminal/session-screen.d.ts +34 -5
  197. package/dist/terminal/session-screen.d.ts.map +1 -1
  198. package/dist/terminal/session-screen.js +52 -6
  199. package/dist/terminal/session-screen.js.map +1 -1
  200. package/dist/terminal/shellper-client.d.ts +73 -0
  201. package/dist/terminal/shellper-client.d.ts.map +1 -1
  202. package/dist/terminal/shellper-client.js +151 -0
  203. package/dist/terminal/shellper-client.js.map +1 -1
  204. package/dist/terminal/shellper-main.js +20 -3
  205. package/dist/terminal/shellper-main.js.map +1 -1
  206. package/dist/terminal/shellper-process.d.ts +2 -0
  207. package/dist/terminal/shellper-process.d.ts.map +1 -1
  208. package/dist/terminal/shellper-process.js +17 -1
  209. package/dist/terminal/shellper-process.js.map +1 -1
  210. package/dist/terminal/shellper-protocol.d.ts +28 -0
  211. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  212. package/dist/terminal/shellper-protocol.js.map +1 -1
  213. package/dist/terminal/terminal-replies.d.ts +102 -0
  214. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  215. package/dist/terminal/terminal-replies.js +181 -0
  216. package/dist/terminal/terminal-replies.js.map +1 -0
  217. package/package.json +4 -4
  218. package/scripts/forge/gitea/_lib.sh +214 -0
  219. package/scripts/forge/gitea/issue-comment.sh +8 -1
  220. package/scripts/forge/gitea/issue-view.sh +95 -4
  221. package/scripts/forge/gitea/pr-create.sh +155 -0
  222. package/scripts/forge/gitea/pr-exists.sh +32 -4
  223. package/scripts/forge/gitea/pr-list.sh +40 -31
  224. package/scripts/forge/gitea/pr-view.sh +65 -4
  225. package/scripts/forge/gitea/recently-merged.sh +91 -21
  226. package/scripts/forge/gitea/user-identity.sh +30 -1
  227. package/scripts/forge/github/issue-view.sh +5 -2
  228. package/scripts/forge/github/pr-create.sh +47 -0
  229. package/scripts/forge/github/pr-list.sh +6 -2
  230. package/scripts/forge/github/pr-search.sh +6 -2
  231. package/scripts/forge/gitlab/issue-search.sh +1 -0
  232. package/scripts/forge/gitlab/issue-view.sh +24 -4
  233. package/scripts/forge/gitlab/pr-create.sh +53 -0
  234. package/scripts/forge/gitlab/pr-search.sh +37 -2
  235. package/scripts/forge/linear/auth-status.sh +1 -0
  236. package/scripts/forge/linear/issue-comment.sh +1 -0
  237. package/scripts/forge/linear/issue-list.sh +1 -0
  238. package/scripts/forge/linear/issue-search.sh +1 -0
  239. package/scripts/forge/linear/issue-view.sh +14 -2
  240. package/scripts/forge/linear/recently-closed.sh +1 -0
  241. package/scripts/forge/linear/user-identity.sh +1 -0
  242. package/skeleton/.claude/skills/generate-image/SKILL.md +39 -1
  243. package/skeleton/.codex/skills/generate-image/SKILL.md +39 -1
  244. package/skeleton/protocols/air/prompts/pr.md +6 -1
  245. package/skeleton/protocols/aspir/prompts/review.md +6 -1
  246. package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
  247. package/skeleton/protocols/bugfix/protocol.md +2 -1
  248. package/skeleton/protocols/maintain/prompts/review.md +6 -1
  249. package/skeleton/protocols/pir/prompts/review.md +10 -7
  250. package/skeleton/protocols/spir/prompts/review.md +6 -1
  251. package/skeleton/resources/commands/agent-farm.md +28 -3
  252. package/skeleton/roles/consultant.md +32 -0
  253. package/skeleton/templates/AGENTS.md +1 -1
  254. package/skeleton/templates/CLAUDE.md +1 -1
  255. package/templates/tower.html +16 -1
  256. package/dashboard-dist/assets/index-BZRTEMtH.css +0 -32
  257. package/dashboard-dist/assets/index-D3XaKpZW.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * global.db owner lock (Issue #1629).
3
+ *
4
+ * The port bind in tower-server.ts is a SAME-PORT mutex only: a second Tower on
5
+ * a DIFFERENT port can still open the same global.db. That is exactly how the
6
+ * incident happened — a test Tower on port 14733 exported `AGENT_FARM_DIR`
7
+ * instead of `CODEV_AGENT_FARM_DIR` (#1515), so it resolved to the production
8
+ * `~/.agent-farm` and opened the live `global.db` (port and DB path are set by
9
+ * independent inputs: the port is a CLI arg, the DB comes from AGENT_FARM_DIR).
10
+ * Its startup reconcile then reconnected every live shellper socket (a shellper
11
+ * holds ONE client), stealing them from the real Tower on 4100, and the
12
+ * stale-row sweep deleted the rows.
13
+ *
14
+ * This module makes the second Tower REFUSE to start instead. The owner record
15
+ * is an exclusive LOCK FILE sitting next to the exact global.db being contended
16
+ * (`<db path>.tower-owner`), so it travels with that file: cross-`CODEV_AGENT_FARM_DIR`
17
+ * isolation is untouched (an isolated DB carries its own lock), and two isolated
18
+ * test Towers on different DB files never share a lock. Acquisition is an atomic
19
+ * `O_EXCL` create — the filesystem itself resolves a race between two starters. A
20
+ * second Tower reads the lock at boot, before reconcile; if a live Tower owns the
21
+ * DB it refuses loudly, otherwise it claims. A dead owner self-clears — a crashed
22
+ * Tower's lock has a dead pid, so the legitimate restart just reclaims.
23
+ *
24
+ * The lock is deliberately NOT a table in global.db: ownership is ephemeral
25
+ * process state (a cleanly-stopped Tower leaves nothing behind), so it needs no
26
+ * durable schema, no migration, and it sits naturally alongside the other
27
+ * runtime artifacts in AGENT_FARM_DIR (`tower.log`, `local-key`).
28
+ */
29
+ /** The owner record persisted in the lock file next to global.db. */
30
+ export interface TowerOwner {
31
+ /** pid of the Tower process that owns this global.db. */
32
+ pid: number;
33
+ /** Port that Tower is listening on (the liveness probe target). */
34
+ port: number;
35
+ /** Machine the owning Tower runs on (diagnostic only). */
36
+ hostname: string;
37
+ /** When ownership was claimed (epoch ms; also the compare-and-set discriminator). */
38
+ startedAt: number;
39
+ /** Resolved AGENT_FARM_DIR the owner opened — surfaced in the conflict error. */
40
+ dbDir: string;
41
+ /**
42
+ * Interface the owner listens on — the liveness probe's target host. Default
43
+ * `127.0.0.1`; bridge mode may bind a specific non-loopback host, and probing
44
+ * loopback would then miss it (a false "gone" → hijack). `0.0.0.0`/`::` are
45
+ * resolved back to loopback for the probe.
46
+ */
47
+ bindHost: string;
48
+ }
49
+ /**
50
+ * The lock file for the active global.db: the DB path plus `.tower-owner`.
51
+ *
52
+ * The suffix is deliberately NOT `.lock`: SQLite's `unix-dotfile` VFS (a fallback
53
+ * on filesystems without POSIX locks) creates `<db>.lock`, which would collide.
54
+ * `.tower-owner` avoids every SQLite auxiliary name (`-wal`, `-shm`, `-journal`,
55
+ * `-mj*`, and the dotfile VFS's `.lock`).
56
+ */
57
+ export declare function defaultLockFile(): string;
58
+ /**
59
+ * Read the owner record from the lock file, or null if the lock is absent or its
60
+ * contents are unusable (corrupt / a partial write from a crashed acquire — the
61
+ * caller clears and reclaims those). A genuine IO error other than "not found"
62
+ * (permission, EISDIR) is rethrown so the boot guard fails CLOSED.
63
+ */
64
+ export declare function readTowerOwner(lockFile: string): TowerOwner | null;
65
+ /** Overwrite the lock file with `owner` (non-atomic; for takeover seams / tests). */
66
+ export declare function writeTowerOwner(lockFile: string, owner: TowerOwner): void;
67
+ /**
68
+ * Remove the lock file, but ONLY if it is still ours (pid match). Best-effort: a
69
+ * hard kill (SIGKILL) never reaches this, and the dead-pid liveness check
70
+ * self-clears the stale lock on the next start regardless. Called on graceful
71
+ * shutdown so the lock reflects reality between a clean stop and the next start.
72
+ */
73
+ export declare function releaseTowerOwnerIfMine(lockFile: string, pid: number): void;
74
+ /**
75
+ * The three distinguishable states of the owner's `/health` port.
76
+ * - `tower`: a live Tower answered — a healthy 200, OR a 503 `STARTING_UP` while
77
+ * its readiness gate holds requests mid-boot. Both mean a live Tower owns it.
78
+ * - `gone`: proof the owner is NOT here — the connection was refused (port free),
79
+ * or a non-Tower process answered (a squatter on a recycled port/pid). Claim.
80
+ * - `unreachable`: inconclusive — a timeout or transport error. A live Tower
81
+ * under load (its `/health` shells out to `ps -A`) or mid-boot can miss the
82
+ * budget, so this must NOT be read as "gone". The caller treats it as live.
83
+ */
84
+ export type HealthProbe = 'tower' | 'gone' | 'unreachable';
85
+ /**
86
+ * Probe `GET /health` on `host`:`port` (unauthenticated per server-utils).
87
+ *
88
+ * The budget is generous by default: `/health` calls `getInstances()` and shells
89
+ * out to `ps -A` (a 5s timeout of its own), so under the incident's load — 60+
90
+ * shellpers, a busy event loop — a tight budget would time out and be misread as
91
+ * "no Tower here", failing OPEN on the one guard that prevents data loss. Only
92
+ * paid on the conflict path (a live pid on a different port), so it costs nothing
93
+ * on the common restart.
94
+ */
95
+ export declare function probeTowerHealth(host: string, port: number, timeoutMs?: number): Promise<HealthProbe>;
96
+ /**
97
+ * Resolve the host to probe from the owner's recorded bind interface. A wildcard
98
+ * bind (`0.0.0.0` / `::` / bracketed `[::]` as validateHost emits) accepts
99
+ * loopback, so probe `127.0.0.1`; a specific bridge host is probed directly
100
+ * (probing loopback would falsely read 'gone').
101
+ */
102
+ export declare function resolveProbeHost(bindHost: string | null | undefined): string;
103
+ /** Injectable liveness seams — real implementations by default; fakes in tests. */
104
+ export interface OwnerLivenessDeps {
105
+ isAlive?: (pid: number) => boolean;
106
+ probe?: (host: string, port: number) => Promise<HealthProbe>;
107
+ }
108
+ /**
109
+ * Decide whether the recorded owner is a live Tower this process must yield to.
110
+ *
111
+ * - same port AND same bind interface as us: our own `server.listen` succeeded
112
+ * on that exact interface:port, and an identical bind is exclusive, so no other
113
+ * process holds it — any prior owner recorded there is gone. Claim. The bind
114
+ * interface must match: a wildcard bind (`0.0.0.0:P`) and a loopback bind
115
+ * (`127.0.0.1:P`) COEXIST on the same port, so `port` alone matching does not
116
+ * prove a differently-bound owner is gone — that path falls through to the probe.
117
+ * - dead owner pid: stale (a crashed Tower / a clean stop that outran the
118
+ * release). Claim — this is the self-clearing legitimate restart.
119
+ * - live pid: probe the owner's port. Only a `gone` result (connection refused,
120
+ * or a non-Tower response) proves the owner is absent → claim. A `tower`
121
+ * response, OR an `unreachable` inconclusive probe against a still-live pid, is
122
+ * treated as a live owner → refuse. This fails CLOSED: a loaded or mid-boot
123
+ * Tower whose `/health` misses the budget is never mistaken for a free DB.
124
+ */
125
+ export declare function ownerIsLive(owner: TowerOwner, myPort: number, myBindHost: string, deps?: OwnerLivenessDeps): Promise<boolean>;
126
+ /** Outcome of {@link claimGlobalDbOwnership}. */
127
+ export type ClaimResult = {
128
+ ok: true;
129
+ } | {
130
+ ok: false;
131
+ conflict: TowerOwner;
132
+ };
133
+ /**
134
+ * Boot guard: refuse if the DB already has a LIVE owner, otherwise claim
135
+ * ownership by writing the lock file. A decision plus a lock write — the caller
136
+ * decides how to fail loudly (log + exit).
137
+ *
138
+ * Acquisition is atomic against a concurrent second Tower: {@link tryAcquire}
139
+ * writes a full temp file and hard-links it onto the path, so the filesystem
140
+ * resolves a two-starter race (one link wins, the other gets EEXIST) with no
141
+ * empty-file window. A stale lock (dead / absent owner) is cleared with a
142
+ * compare-and-set on its contents and the link retried; the invariant preserved
143
+ * is "never remove a LIVE owner's lock". Under persistent churn the claim refuses
144
+ * rather than forcing the lock.
145
+ */
146
+ export declare function claimGlobalDbOwnership(opts: {
147
+ /** Lock file path; defaults to the active global.db's `.lock` sibling. */
148
+ lockFile?: string;
149
+ pid: number;
150
+ port: number;
151
+ dbDir: string;
152
+ /** Interface this Tower listens on (default `127.0.0.1`); persisted as the probe target. */
153
+ bindHost?: string;
154
+ deps?: OwnerLivenessDeps;
155
+ }): Promise<ClaimResult>;
156
+ /**
157
+ * The loud, teaching error for a refused start. Names the live owner (pid/port/
158
+ * host) and the shared DB, and — because the misconfiguration that triggered the
159
+ * incident was a mistyped env var — spells out the `AGENT_FARM_DIR` vs
160
+ * `CODEV_AGENT_FARM_DIR` fix.
161
+ *
162
+ * This string is user-visible at the CLI (surfaced by the launcher on a fast-exit,
163
+ * Issue #1691), so it carries no internal issue numbers — the incident context
164
+ * (#1629's shellper hijack, the #1515 mistyped-env-var trigger) stays here in the
165
+ * comment, not in the text a user reads.
166
+ */
167
+ export declare function ownershipConflictMessage(conflict: TowerOwner, dbDir: string): string;
168
+ //# sourceMappingURL=tower-owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tower-owner.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/tower-owner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAOD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAsBlE;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAGzE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAO3E;AAYD;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,aAAa,CAAC;AAE3D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CA6DnG;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAQ5E;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,iDAAiD;AACjD,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAA;CAAE,CAAC;AAiExC;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B,GAAG,OAAO,CAAC,WAAW,CAAC,CAoCvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASpF"}
@@ -0,0 +1,369 @@
1
+ /**
2
+ * global.db owner lock (Issue #1629).
3
+ *
4
+ * The port bind in tower-server.ts is a SAME-PORT mutex only: a second Tower on
5
+ * a DIFFERENT port can still open the same global.db. That is exactly how the
6
+ * incident happened — a test Tower on port 14733 exported `AGENT_FARM_DIR`
7
+ * instead of `CODEV_AGENT_FARM_DIR` (#1515), so it resolved to the production
8
+ * `~/.agent-farm` and opened the live `global.db` (port and DB path are set by
9
+ * independent inputs: the port is a CLI arg, the DB comes from AGENT_FARM_DIR).
10
+ * Its startup reconcile then reconnected every live shellper socket (a shellper
11
+ * holds ONE client), stealing them from the real Tower on 4100, and the
12
+ * stale-row sweep deleted the rows.
13
+ *
14
+ * This module makes the second Tower REFUSE to start instead. The owner record
15
+ * is an exclusive LOCK FILE sitting next to the exact global.db being contended
16
+ * (`<db path>.tower-owner`), so it travels with that file: cross-`CODEV_AGENT_FARM_DIR`
17
+ * isolation is untouched (an isolated DB carries its own lock), and two isolated
18
+ * test Towers on different DB files never share a lock. Acquisition is an atomic
19
+ * `O_EXCL` create — the filesystem itself resolves a race between two starters. A
20
+ * second Tower reads the lock at boot, before reconcile; if a live Tower owns the
21
+ * DB it refuses loudly, otherwise it claims. A dead owner self-clears — a crashed
22
+ * Tower's lock has a dead pid, so the legitimate restart just reclaims.
23
+ *
24
+ * The lock is deliberately NOT a table in global.db: ownership is ephemeral
25
+ * process state (a cleanly-stopped Tower leaves nothing behind), so it needs no
26
+ * durable schema, no migration, and it sits naturally alongside the other
27
+ * runtime artifacts in AGENT_FARM_DIR (`tower.log`, `local-key`).
28
+ */
29
+ import http from 'node:http';
30
+ import os from 'node:os';
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ import crypto from 'node:crypto';
34
+ import { getGlobalDbPath } from './index.js';
35
+ /**
36
+ * The lock file for the active global.db: the DB path plus `.tower-owner`.
37
+ *
38
+ * The suffix is deliberately NOT `.lock`: SQLite's `unix-dotfile` VFS (a fallback
39
+ * on filesystems without POSIX locks) creates `<db>.lock`, which would collide.
40
+ * `.tower-owner` avoids every SQLite auxiliary name (`-wal`, `-shm`, `-journal`,
41
+ * `-mj*`, and the dotfile VFS's `.lock`).
42
+ */
43
+ export function defaultLockFile() {
44
+ return `${getGlobalDbPath()}.tower-owner`;
45
+ }
46
+ /** Serialize an owner record to the lock file's on-disk form. */
47
+ function serialize(owner) {
48
+ return JSON.stringify(owner);
49
+ }
50
+ /**
51
+ * Read the owner record from the lock file, or null if the lock is absent or its
52
+ * contents are unusable (corrupt / a partial write from a crashed acquire — the
53
+ * caller clears and reclaims those). A genuine IO error other than "not found"
54
+ * (permission, EISDIR) is rethrown so the boot guard fails CLOSED.
55
+ */
56
+ export function readTowerOwner(lockFile) {
57
+ let raw;
58
+ try {
59
+ raw = fs.readFileSync(lockFile, 'utf8');
60
+ }
61
+ catch (err) {
62
+ if (err.code === 'ENOENT')
63
+ return null;
64
+ throw err;
65
+ }
66
+ try {
67
+ const o = JSON.parse(raw);
68
+ if (typeof o.pid !== 'number' || typeof o.port !== 'number')
69
+ return null;
70
+ return {
71
+ pid: o.pid,
72
+ port: o.port,
73
+ hostname: typeof o.hostname === 'string' ? o.hostname : 'unknown',
74
+ startedAt: typeof o.startedAt === 'number' ? o.startedAt : 0,
75
+ dbDir: typeof o.dbDir === 'string' ? o.dbDir : '',
76
+ bindHost: typeof o.bindHost === 'string' ? o.bindHost : '127.0.0.1',
77
+ };
78
+ }
79
+ catch {
80
+ return null; // unparseable → treat as a corrupt lock the caller can clear
81
+ }
82
+ }
83
+ /** Overwrite the lock file with `owner` (non-atomic; for takeover seams / tests). */
84
+ export function writeTowerOwner(lockFile, owner) {
85
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
86
+ fs.writeFileSync(lockFile, serialize(owner));
87
+ }
88
+ /**
89
+ * Remove the lock file, but ONLY if it is still ours (pid match). Best-effort: a
90
+ * hard kill (SIGKILL) never reaches this, and the dead-pid liveness check
91
+ * self-clears the stale lock on the next start regardless. Called on graceful
92
+ * shutdown so the lock reflects reality between a clean stop and the next start.
93
+ */
94
+ export function releaseTowerOwnerIfMine(lockFile, pid) {
95
+ try {
96
+ const cur = readTowerOwner(lockFile);
97
+ if (cur && cur.pid === pid)
98
+ fs.rmSync(lockFile, { force: true });
99
+ }
100
+ catch {
101
+ // Lock gone / unreadable — nothing to release.
102
+ }
103
+ }
104
+ /** Whether a pid is a live process (SIGnal 0 probe, same as processExists). */
105
+ function pidAlive(pid) {
106
+ try {
107
+ process.kill(pid, 0);
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }
114
+ /**
115
+ * Probe `GET /health` on `host`:`port` (unauthenticated per server-utils).
116
+ *
117
+ * The budget is generous by default: `/health` calls `getInstances()` and shells
118
+ * out to `ps -A` (a 5s timeout of its own), so under the incident's load — 60+
119
+ * shellpers, a busy event loop — a tight budget would time out and be misread as
120
+ * "no Tower here", failing OPEN on the one guard that prevents data loss. Only
121
+ * paid on the conflict path (a live pid on a different port), so it costs nothing
122
+ * on the common restart.
123
+ */
124
+ export function probeTowerHealth(host, port, timeoutMs = 6000) {
125
+ return new Promise((resolve) => {
126
+ const req = http.request({ hostname: host, port, path: '/health', method: 'GET', timeout: timeoutMs }, (res) => {
127
+ let body = '';
128
+ res.setEncoding('utf8');
129
+ res.on('data', (chunk) => {
130
+ body += chunk;
131
+ // A tower /health payload is small; cap the read so a chatty non-tower
132
+ // process on the port can't stream unboundedly.
133
+ if (body.length > 64_000) {
134
+ res.destroy();
135
+ resolve('gone');
136
+ }
137
+ });
138
+ res.on('end', () => {
139
+ if (res.statusCode === 200) {
140
+ try {
141
+ resolve(JSON.parse(body).status === 'healthy' ? 'tower' : 'gone');
142
+ }
143
+ catch {
144
+ resolve('gone');
145
+ }
146
+ return;
147
+ }
148
+ if (res.statusCode === 503) {
149
+ // A live Tower can answer 503 {"error":"STARTING_UP"} from its readiness
150
+ // gate. In practice a mid-boot Tower usually HOLDS /health past this
151
+ // probe's budget, so that case times out to 'unreachable' (also refused);
152
+ // this branch covers a 503 returned within budget. Either way, a live owner.
153
+ try {
154
+ resolve(JSON.parse(body).error === 'STARTING_UP' ? 'tower' : 'unreachable');
155
+ }
156
+ catch {
157
+ resolve('unreachable');
158
+ }
159
+ return;
160
+ }
161
+ // Any other 5xx: a degraded-but-live server may answer 500 rather than a
162
+ // clean /health. Treat as inconclusive (fail closed), never as absent.
163
+ if (res.statusCode && res.statusCode >= 500) {
164
+ resolve('unreachable');
165
+ return;
166
+ }
167
+ // A non-5xx, non-Tower HTTP response — a process answered, but it is not
168
+ // a Tower serving /health (a squatter on a recycled port).
169
+ resolve('gone');
170
+ });
171
+ });
172
+ req.on('error', (err) => {
173
+ // ECONNREFUSED = nothing is listening on the port, so the recorded owner is
174
+ // not serving it (a live Tower always holds its port) → gone. Any other
175
+ // transport error is inconclusive → unreachable (fail closed).
176
+ resolve(err.code === 'ECONNREFUSED' ? 'gone' : 'unreachable');
177
+ });
178
+ req.on('timeout', () => {
179
+ req.destroy();
180
+ resolve('unreachable');
181
+ });
182
+ req.end();
183
+ });
184
+ }
185
+ /**
186
+ * Resolve the host to probe from the owner's recorded bind interface. A wildcard
187
+ * bind (`0.0.0.0` / `::` / bracketed `[::]` as validateHost emits) accepts
188
+ * loopback, so probe `127.0.0.1`; a specific bridge host is probed directly
189
+ * (probing loopback would falsely read 'gone').
190
+ */
191
+ export function resolveProbeHost(bindHost) {
192
+ if (!bindHost || bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
193
+ return '127.0.0.1';
194
+ // A specific bracketed IPv6 literal ([::1], [2001:db8::1]): http.request's
195
+ // `hostname` wants the address WITHOUT brackets, or it rejects it and the probe
196
+ // is never conclusive. Strip them.
197
+ const ipv6 = bindHost.match(/^\[(.+)\]$/);
198
+ if (ipv6)
199
+ return ipv6[1];
200
+ return bindHost;
201
+ }
202
+ /**
203
+ * Decide whether the recorded owner is a live Tower this process must yield to.
204
+ *
205
+ * - same port AND same bind interface as us: our own `server.listen` succeeded
206
+ * on that exact interface:port, and an identical bind is exclusive, so no other
207
+ * process holds it — any prior owner recorded there is gone. Claim. The bind
208
+ * interface must match: a wildcard bind (`0.0.0.0:P`) and a loopback bind
209
+ * (`127.0.0.1:P`) COEXIST on the same port, so `port` alone matching does not
210
+ * prove a differently-bound owner is gone — that path falls through to the probe.
211
+ * - dead owner pid: stale (a crashed Tower / a clean stop that outran the
212
+ * release). Claim — this is the self-clearing legitimate restart.
213
+ * - live pid: probe the owner's port. Only a `gone` result (connection refused,
214
+ * or a non-Tower response) proves the owner is absent → claim. A `tower`
215
+ * response, OR an `unreachable` inconclusive probe against a still-live pid, is
216
+ * treated as a live owner → refuse. This fails CLOSED: a loaded or mid-boot
217
+ * Tower whose `/health` misses the budget is never mistaken for a free DB.
218
+ */
219
+ export async function ownerIsLive(owner, myPort, myBindHost, deps = {}) {
220
+ const isAlive = deps.isAlive ?? pidAlive;
221
+ const probe = deps.probe ?? probeTowerHealth;
222
+ if (owner.port === myPort && owner.bindHost === myBindHost)
223
+ return false;
224
+ if (!isAlive(owner.pid))
225
+ return false;
226
+ return (await probe(resolveProbeHost(owner.bindHost), owner.port)) !== 'gone';
227
+ }
228
+ /**
229
+ * Bound on the acquire attempts under contention. Each losing attempt means
230
+ * another Tower held or rewrote the lock between our checks; a handful of retries
231
+ * converges in every realistic case, and exhausting them means the lock is
232
+ * churning between many simultaneous starts — where the safe answer is to refuse
233
+ * (fail closed), never to force the lock.
234
+ */
235
+ const MAX_CLAIM_ATTEMPTS = 5;
236
+ /**
237
+ * Atomically acquire the lock: write the FULL record to a unique temp file, then
238
+ * hard-link it onto the lock path. `link()` is atomic and fails with EEXIST if
239
+ * the path is already taken, and — unlike `writeFile({flag:'wx'})` — the linked
240
+ * file already has its complete content, so a concurrent reader never sees an
241
+ * empty/partial lock it could misclassify as corrupt and delete mid-write. The
242
+ * temp is always cleaned up (the hard link keeps the inode alive at the lock path).
243
+ * Returns whether we won the link.
244
+ */
245
+ function tryAcquire(lockFile, owner) {
246
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
247
+ const tmp = `${lockFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
248
+ fs.writeFileSync(tmp, serialize(owner));
249
+ try {
250
+ fs.linkSync(tmp, lockFile);
251
+ return true;
252
+ }
253
+ catch (err) {
254
+ const code = err.code;
255
+ if (code === 'EEXIST')
256
+ return false;
257
+ // Some filesystems don't support hard links (ENOSYS/EPERM, or a cross-device
258
+ // temp on EXDEV). Fall back to an O_EXCL create — a hair weaker (a brief
259
+ // empty-file window) but better than failing to boot at all.
260
+ if (code === 'ENOSYS' || code === 'EPERM' || code === 'EXDEV') {
261
+ try {
262
+ fs.writeFileSync(lockFile, serialize(owner), { flag: 'wx' });
263
+ return true;
264
+ }
265
+ catch (fallbackErr) {
266
+ if (fallbackErr.code === 'EEXIST')
267
+ return false;
268
+ throw fallbackErr;
269
+ }
270
+ }
271
+ throw err;
272
+ }
273
+ finally {
274
+ try {
275
+ fs.rmSync(tmp, { force: true });
276
+ }
277
+ catch { /* temp already gone */ }
278
+ }
279
+ }
280
+ /**
281
+ * Remove a stale lock, but ONLY if it still names the holder we probed (a
282
+ * compare-and-set on pid + startedAt). If a contender rewrote it in between, the
283
+ * content differs and we do NOT remove it — we loop and re-evaluate. Returns
284
+ * whether we removed it.
285
+ */
286
+ function removeStaleLock(lockFile, observed) {
287
+ const cur = readTowerOwner(lockFile);
288
+ if (!cur || cur.pid !== observed.pid || cur.startedAt !== observed.startedAt)
289
+ return false;
290
+ try {
291
+ fs.rmSync(lockFile, { force: true });
292
+ return true;
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
298
+ /**
299
+ * Boot guard: refuse if the DB already has a LIVE owner, otherwise claim
300
+ * ownership by writing the lock file. A decision plus a lock write — the caller
301
+ * decides how to fail loudly (log + exit).
302
+ *
303
+ * Acquisition is atomic against a concurrent second Tower: {@link tryAcquire}
304
+ * writes a full temp file and hard-links it onto the path, so the filesystem
305
+ * resolves a two-starter race (one link wins, the other gets EEXIST) with no
306
+ * empty-file window. A stale lock (dead / absent owner) is cleared with a
307
+ * compare-and-set on its contents and the link retried; the invariant preserved
308
+ * is "never remove a LIVE owner's lock". Under persistent churn the claim refuses
309
+ * rather than forcing the lock.
310
+ */
311
+ export async function claimGlobalDbOwnership(opts) {
312
+ const lockFile = opts.lockFile ?? defaultLockFile();
313
+ const { pid, port, dbDir, deps } = opts;
314
+ const bindHost = opts.bindHost ?? '127.0.0.1';
315
+ let lastSeen = null;
316
+ for (let attempt = 0; attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
317
+ const mine = { pid, port, hostname: os.hostname(), startedAt: Date.now(), dbDir, bindHost };
318
+ // Atomic acquire: full-content temp + hard link (see tryAcquire).
319
+ if (tryAcquire(lockFile, mine))
320
+ return { ok: true };
321
+ // Lock exists — evaluate the current holder.
322
+ const existing = readTowerOwner(lockFile);
323
+ if (!existing) {
324
+ // Corrupt / vanished under us — clear it and retry the atomic acquire.
325
+ try {
326
+ fs.rmSync(lockFile, { force: true });
327
+ }
328
+ catch { /* already gone */ }
329
+ continue;
330
+ }
331
+ lastSeen = existing;
332
+ if (await ownerIsLive(existing, port, bindHost, deps)) {
333
+ return { ok: false, conflict: existing };
334
+ }
335
+ // Stale — remove ONLY if the lock still names the holder we probed, then loop
336
+ // to re-attempt the atomic acquire (the link is the serialization point). A
337
+ // failed CAS means a contender rewrote it: loop and re-evaluate, never force.
338
+ removeStaleLock(lockFile, existing);
339
+ }
340
+ // Persistent contention: the lock keeps changing under us. Refuse rather than
341
+ // risk forcing over a live claimant that appeared mid-flight. Name a real
342
+ // contender we observed, never ourselves (Claude CMAP).
343
+ const conflict = readTowerOwner(lockFile) ?? lastSeen ?? {
344
+ pid: -1, port, hostname: 'unknown (lock contended)', startedAt: Date.now(), dbDir, bindHost,
345
+ };
346
+ return { ok: false, conflict };
347
+ }
348
+ /**
349
+ * The loud, teaching error for a refused start. Names the live owner (pid/port/
350
+ * host) and the shared DB, and — because the misconfiguration that triggered the
351
+ * incident was a mistyped env var — spells out the `AGENT_FARM_DIR` vs
352
+ * `CODEV_AGENT_FARM_DIR` fix.
353
+ *
354
+ * This string is user-visible at the CLI (surfaced by the launcher on a fast-exit,
355
+ * Issue #1691), so it carries no internal issue numbers — the incident context
356
+ * (#1629's shellper hijack, the #1515 mistyped-env-var trigger) stays here in the
357
+ * comment, not in the text a user reads.
358
+ */
359
+ export function ownershipConflictMessage(conflict, dbDir) {
360
+ return [
361
+ `Refusing to start: this global.db is already owned by a live Tower.`,
362
+ ` owner pid ${conflict.pid} on port ${conflict.port} (host ${conflict.hostname})`,
363
+ ` shared database dir: ${dbDir}`,
364
+ `A second Tower opening a live global.db hijacks and deletes its shellper sessions.`,
365
+ `If you meant to run an isolated test Tower, set CODEV_AGENT_FARM_DIR (NOT AGENT_FARM_DIR) to a throwaway directory.`,
366
+ `Otherwise stop the existing Tower first ('afx tower stop'), or investigate pid ${conflict.pid} if you believe it is stale.`,
367
+ ].join('\n');
368
+ }
369
+ //# sourceMappingURL=tower-owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tower-owner.js","sourceRoot":"","sources":["../../../src/agent-farm/db/tower-owner.ts"],"names":[],"mappings":"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"}
@@ -84,6 +84,52 @@ export type MailboxStatus = 'held' | 'delivered' | 'superseded' | 'dismissed';
84
84
  * - `no-live-pty` — the recipient agent has no live terminal right now
85
85
  */
86
86
  export type MailboxReason = 'busy' | 'no-profile' | 'no-live-pty';
87
+ /**
88
+ * The render gate's classification detail behind a `busy` hold (Issue #1482). Persisted
89
+ * beside {@link MailboxReason} so every operator surface can tell the two apart:
90
+ * - `user-text` — a draft or menu occupies the composer. A human is at the line;
91
+ * this is the SAFE, intended hold and it clears when they finish.
92
+ * - `no-region-end` — a composer marker with no rule/status line beneath it to bound
93
+ * the region (a partial/mid-repaint frame, or a mirror rendered at
94
+ * dims the real TUI never adopted).
95
+ * - `no-composer-marker` — no recognized marker at all (a wrapper/boot screen, a drifted
96
+ * profile, or an unrenderable frame).
97
+ * - `recent-input` — the terminal saw INPUT too recently to write onto (Issue #1473):
98
+ * either it landed while the gate was classifying, or the
99
+ * input-settle interval has not elapsed. "Input" is whatever
100
+ * survives `stripTerminalReplies` on a write reaching the PTY
101
+ * under `PtySession.write()`'s default `'external'` origin — a
102
+ * human's keystrokes, and equally an ungated `--interrupt`/
103
+ * `--escape`, which is why the operator wording says "input"
104
+ * rather than "typing"; a chunk that is nothing but a terminal
105
+ * REPLY moves neither signal. Like `user-text` this
106
+ * is a SAFE hold that clears on its own — the same someone-else-is-
107
+ * writing class, caught one beat earlier, before the TUI has echoed
108
+ * anything for the classifier to see.
109
+ * - `composer-redraw` — the composer is EMPTY but its rendered region changed within the
110
+ * settle window (Issue #1664): a turn-end box redraw, a rotating
111
+ * placeholder, a resize, or an interrupt clearing the line. The
112
+ * recipient's own output ELSEWHERE on the screen — a spinner, a
113
+ * streaming transcript — is deliberately NOT this: writing onto an
114
+ * empty composer while the agent talks is safe and intended, since
115
+ * claude/codex queue input received mid-turn. Self-clearing, like
116
+ * `user-text` and `recent-input`; it resolves about a settle after
117
+ * the composer stops moving.
118
+ * The two can't-verify values are the DEFECT class: the classifier could not verify anything,
119
+ * so the mail will not deliver on its own. `GateVerdict.detail`'s fourth value, `empty`, is
120
+ * never persisted — a clean verdict delivers the row (and delivery nulls both columns).
121
+ *
122
+ * `recent-input` and `composer-redraw` are deliberately NOT part of {@link isUnverifiableVerdict}'s
123
+ * set: escalating either would false-alarm on every ordinary typist and every ordinary repaint. That predicate is an allow-list, so the value is
124
+ * correctly inert there by construction rather than by an edit.
125
+ *
126
+ * Null for every non-gate hold (`no-live-pty`, `no-profile`) and for the post-classify
127
+ * token re-hold, so a stale detail can never outlive the verdict that produced it.
128
+ *
129
+ * The DB column carries NO CHECK constraint (see `GLOBAL_SCHEMA` / migration v18); this type
130
+ * is the enforcement.
131
+ */
132
+ export type MailboxGateDetail = 'user-text' | 'no-region-end' | 'no-composer-marker' | 'recent-input' | 'composer-redraw';
87
133
  /**
88
134
  * Database row type for the mailbox table (Spec 1313).
89
135
  *
@@ -105,6 +151,7 @@ export interface DbMailbox {
105
151
  no_enter: number;
106
152
  status: MailboxStatus;
107
153
  reason: MailboxReason | null;
154
+ detail: MailboxGateDetail | null;
108
155
  supersede_key: string | null;
109
156
  escalated: number;
110
157
  not_before: number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsHH;;GAEG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAgB;IAC1D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,SAAS,EAAE,GAAG,CAAC,UAAU,IAAI,SAAS;KACvC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAA2B;QACvC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,IAAmB;QAC7B,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;QACpC,YAAY,EAAE,GAAG,CAAC,aAAa,IAAI,SAAS;QAC5C,WAAW,EAAE,GAAG,CAAC,YAAY,IAAI,SAAS;QAC1C,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,kBAAkB,EAAE,GAAG,CAAC,oBAAoB,IAAI,SAAS;KAC1D,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;KACzC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAiB;IACxD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE;YACN,IAAI,EAAE,GAAG,CAAC,WAA2C;YACrD,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;SAC/B;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2KH;;GAEG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAgB;IAC1D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,SAAS,EAAE,GAAG,CAAC,UAAU,IAAI,SAAS;KACvC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAA2B;QACvC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,IAAmB;QAC7B,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;QACpC,YAAY,EAAE,GAAG,CAAC,aAAa,IAAI,SAAS;QAC5C,WAAW,EAAE,GAAG,CAAC,YAAY,IAAI,SAAS;QAC1C,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,kBAAkB,EAAE,GAAG,CAAC,oBAAoB,IAAI,SAAS;KAC1D,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;KACzC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAiB;IACxD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE;YACN,IAAI,EAAE,GAAG,CAAC,WAA2C;YACrD,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;SAC/B;KACF,CAAC;AACJ,CAAC"}