@cluesmith/codev 3.3.3 → 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.
- package/dashboard-dist/assets/{index-CUIjS_Vb.js → index-DZf6pNPE.js} +2 -2
- package/dashboard-dist/assets/{index-CUIjS_Vb.js.map → index-DZf6pNPE.js.map} +1 -1
- package/dashboard-dist/index.html +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +48 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/ide.d.ts +20 -0
- package/dist/agent-farm/commands/ide.d.ts.map +1 -0
- package/dist/agent-farm/commands/ide.js +138 -0
- package/dist/agent-farm/commands/ide.js.map +1 -0
- package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
- package/dist/agent-farm/commands/inbox.js +15 -3
- package/dist/agent-farm/commands/inbox.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +16 -1
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +16 -7
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts +21 -9
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +90 -30
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/tower-owner.d.ts +168 -0
- package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
- package/dist/agent-farm/db/tower-owner.js +369 -0
- package/dist/agent-farm/db/tower-owner.js.map +1 -0
- package/dist/agent-farm/db/types.d.ts +30 -5
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/lib/ide-record.d.ts +65 -0
- package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-record.js +118 -0
- package/dist/agent-farm/lib/ide-record.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +53 -5
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.d.ts +23 -2
- package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.js +43 -2
- package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
- package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
- package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-forward.js +274 -0
- package/dist/agent-farm/servers/ide-forward.js.map +1 -0
- package/dist/agent-farm/servers/ide-server.d.ts +68 -0
- package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-server.js +281 -0
- package/dist/agent-farm/servers/ide-server.js.map +1 -0
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +213 -4
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +489 -68
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +18 -4
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +94 -21
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +138 -11
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +144 -35
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +28 -12
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +130 -79
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +53 -0
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +135 -24
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +7 -4
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +7 -4
- package/dist/agent-farm/servers/session-submit.js.map +1 -1
- package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-cron.js +4 -1
- package/dist/agent-farm/servers/tower-cron.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +6 -7
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +53 -13
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +64 -7
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +53 -8
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +18 -1
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +5 -6
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/port.d.ts +22 -0
- package/dist/agent-farm/utils/port.d.ts.map +1 -0
- package/dist/agent-farm/utils/port.js +35 -0
- package/dist/agent-farm/utils/port.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +26 -0
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +36 -0
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +61 -1
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/tree-tripwire.d.ts +177 -0
- package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
- package/dist/commands/consult/tree-tripwire.js +356 -0
- package/dist/commands/consult/tree-tripwire.js.map +1 -0
- package/dist/commands/doctor.d.ts +69 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-image.d.ts +5 -0
- package/dist/commands/generate-image.d.ts.map +1 -1
- package/dist/commands/generate-image.js +225 -4
- package/dist/commands/generate-image.js.map +1 -1
- package/dist/lib/agent-env.d.ts +86 -0
- package/dist/lib/agent-env.d.ts.map +1 -0
- package/dist/lib/agent-env.js +126 -0
- package/dist/lib/agent-env.js.map +1 -0
- package/dist/lib/forge-contracts.d.ts +10 -0
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/forge-rate-limit.d.ts +45 -0
- package/dist/lib/forge-rate-limit.d.ts.map +1 -0
- package/dist/lib/forge-rate-limit.js +89 -0
- package/dist/lib/forge-rate-limit.js.map +1 -0
- package/dist/lib/forge.d.ts +28 -0
- package/dist/lib/forge.d.ts.map +1 -1
- package/dist/lib/forge.js +63 -10
- package/dist/lib/forge.js.map +1 -1
- package/dist/lib/github.d.ts +13 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +58 -15
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/test-env.d.ts +17 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +20 -0
- package/dist/lib/test-env.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +122 -4
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +142 -7
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +23 -2
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +115 -36
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/session-screen.d.ts +34 -5
- package/dist/terminal/session-screen.d.ts.map +1 -1
- package/dist/terminal/session-screen.js +52 -6
- package/dist/terminal/session-screen.js.map +1 -1
- package/dist/terminal/terminal-replies.d.ts +102 -0
- package/dist/terminal/terminal-replies.d.ts.map +1 -0
- package/dist/terminal/terminal-replies.js +181 -0
- package/dist/terminal/terminal-replies.js.map +1 -0
- package/package.json +4 -4
- package/scripts/forge/github/pr-list.sh +6 -2
- package/scripts/forge/github/pr-search.sh +4 -2
- package/scripts/forge/gitlab/issue-search.sh +1 -0
- package/scripts/forge/gitlab/pr-search.sh +35 -2
- package/scripts/forge/linear/auth-status.sh +1 -0
- package/scripts/forge/linear/issue-comment.sh +1 -0
- package/scripts/forge/linear/issue-list.sh +1 -0
- package/scripts/forge/linear/issue-search.sh +1 -0
- package/scripts/forge/linear/issue-view.sh +1 -0
- package/scripts/forge/linear/recently-closed.sh +1 -0
- package/scripts/forge/linear/user-identity.sh +1 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +19 -2
- package/skeleton/.codex/skills/generate-image/SKILL.md +19 -2
- package/skeleton/resources/commands/agent-farm.md +27 -2
- package/skeleton/roles/consultant.md +32 -0
- package/skeleton/templates/AGENTS.md +1 -1
- package/skeleton/templates/CLAUDE.md +1 -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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"}
|
|
@@ -94,17 +94,42 @@ export type MailboxReason = 'busy' | 'no-profile' | 'no-live-pty';
|
|
|
94
94
|
* dims the real TUI never adopted).
|
|
95
95
|
* - `no-composer-marker` — no recognized marker at all (a wrapper/boot screen, a drifted
|
|
96
96
|
* profile, or an unrenderable frame).
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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.
|
|
100
125
|
*
|
|
101
126
|
* Null for every non-gate hold (`no-live-pty`, `no-profile`) and for the post-classify
|
|
102
|
-
* token
|
|
127
|
+
* token re-hold, so a stale detail can never outlive the verdict that produced it.
|
|
103
128
|
*
|
|
104
129
|
* The DB column carries NO CHECK constraint (see `GLOBAL_SCHEMA` / migration v18); this type
|
|
105
130
|
* is the enforcement.
|
|
106
131
|
*/
|
|
107
|
-
export type MailboxGateDetail = 'user-text' | 'no-region-end' | 'no-composer-marker';
|
|
132
|
+
export type MailboxGateDetail = 'user-text' | 'no-region-end' | 'no-composer-marker' | 'recent-input' | 'composer-redraw';
|
|
108
133
|
/**
|
|
109
134
|
* Database row type for the mailbox table (Spec 1313).
|
|
110
135
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAElG;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAElG;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,iBAAiB,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,WAAW,GAAG,cAAc,CAQ5E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAe1D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAM9D;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,UAAU,CAStE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
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"}
|