Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTINUITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Key decisions:
- React UI is primary; server should serve React build when present.
- Agent lane follows vanilla JS behavior: show all open sessions (active + idle) and sort by state rank then CPU.
- Layout uses pinned positions per agent identity; only new agents are placed; existing tiles never move; placement uses max-height bounds to prevent overlap.
- Scale placement with spatial hash buckets (cell -> occupants) and persistent per-group spiral frontier; collisions checked by bounds against bucket occupants.
- Codex prompt-like titles containing temp paths/turn markers are ignored for lane labels; fallback to repo or codex#pid.
- Agent lane now shows only active/error sessions and uses stable sort (state rank then identity) to prevent reordering.
- Active lane items now glow via `is-active` class (lane item box-shadow) to make active codex sessions visibly highlighted.
Expand Down Expand Up @@ -133,6 +134,11 @@ State:
- Tests re-run: `npm run test:unit` (149 pass), `npm run test:integration` (54 pass), `npm run test:ui` (19 pass).
- Build run after pinned layout changes: `npm run build` (pass).
- Copied edited files to clipboard via pbcopy; temp bundle at `/tmp/consensus-edited-files.txt`.
- Implemented spatial hash buckets + persistent group spiral frontier; fixed cell packing and bounds-based collision checks (public/src/lib/layout.ts).
- Renderer now keeps a single RAF loop, exposes hit list, and uses correct view transform for hit tests (public/src/hooks/useCanvasRenderer.ts).
- CanvasScene no longer rebuilds hit list; uses renderer hit list and stable render loop (public/src/components/CanvasScene.tsx).
- Obstruction detection checks only front occluders with AABB guard (public/src/hooks/useCanvasRenderer.ts).
- Tests run after layout/renderer updates: `npm run test:unit` (152 pass), `npm run test:integration` (58 pass), `npm run test:ui` (19 pass).
- Now:
- Answer user question about hot reload configuration (Vite HMR + server watch + live reload SSE).
- Answer whether any additional setup is missing when using Vite.
Expand Down Expand Up @@ -161,6 +167,7 @@ State:
- Claude CLI hooks not firing for real sessions; user reports Claude does not show active when working.
- Need to validate live Claude session while recording via agent-browser and inspect hook delivery.
- Copy edited layout/test files to clipboard via pbcopy. (DONE)
- Implement spatial hash buckets + persistent group frontier with correct cell packing and bounds checks. (DONE)
- User requested a best-practice plan (no code edits) to address review comment: `npm run dev` no longer starts Vite, so TSX client fails unless dev:client or build is run.
- Q&A answered for dev workflow: always start Vite; use CONSENSUS_UI_PORT for port; on port conflict try next port.
- Review plan requested for new P2/P3 items: Codex in-flight timeout default in codexLogs; TOML [tui] notifications insertion in cli/setup.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,19 @@ consensus dev server running on http://127.0.0.1:8787
- `CONSENSUS_OPENCODE_EVENT_ACTIVE_MS`: OpenCode active window after last event in ms (default `0`).
- `CONSENSUS_OPENCODE_ACTIVE_HOLD_MS`: OpenCode hold window in ms (default `3000`).
- `CONSENSUS_OPENCODE_INFLIGHT_IDLE_MS`: OpenCode in-flight idle timeout in ms (defaults to `CONSENSUS_OPENCODE_INFLIGHT_TIMEOUT_MS`).
- `CONSENSUS_OPENCODE_INFLIGHT_TIMEOUT_MS`: OpenCode hard in-flight timeout in ms (default `15000`).
- `CONSENSUS_OPENCODE_INFLIGHT_TIMEOUT_MS`: OpenCode hard in-flight timeout in ms (default `2500`).
- `CONSENSUS_OPENCODE_INFLIGHT_STALE_MS`: OpenCode stale in-flight cutoff in ms (default `0`).
- `CONSENSUS_PROCESS_MATCH`: regex to match codex processes.
- `CONSENSUS_REDACT_PII`: set to `0` to disable redaction (default enabled).
- `CONSENSUS_UI_PORT`: dev UI port for Vite when running `npm run dev` (default `5173`).
- `CONSENSUS_DEBUG_OPENCODE`: set to `1` to log OpenCode server discovery.
- `CONSENSUS_CODEX_EVENT_ACTIVE_MS`: Codex active window after last event in ms (default `30000`).
- `CONSENSUS_CODEX_ACTIVE_HOLD_MS`: Codex hold window in ms (default `3000`).
- `CONSENSUS_CODEX_ACTIVE_HOLD_MS`: Codex hold window in ms (default `2000`, set to `0` to disable).
- `CONSENSUS_CODEX_INFLIGHT_IDLE_MS`: Codex in-flight idle timeout in ms (default `30000`, set to `0` to disable).
- `CONSENSUS_CODEX_CPU_SUSTAIN_MS`: sustained CPU window before Codex becomes active without logs (default `500`).
- `CONSENSUS_CODEX_CPU_SPIKE`: Codex CPU spike threshold for immediate activation (default derived).
- `CONSENSUS_CODEX_INFLIGHT_TIMEOUT_MS`: Codex in-flight timeout in ms (default `3000`).
- `CONSENSUS_CODEX_SIGNAL_MAX_AGE_MS`: Codex max event age for in-flight signals (default `CONSENSUS_CODEX_INFLIGHT_TIMEOUT_MS`).
- `CONSENSUS_CODEX_INFLIGHT_TIMEOUT_MS`: Codex in-flight timeout in ms (default `2500`).
- `CONSENSUS_CODEX_SIGNAL_MAX_AGE_MS`: Codex max event age for in-flight signals (default `max(CONSENSUS_CODEX_INFLIGHT_TIMEOUT_MS, 2500)`).
- `CONSENSUS_PROCESS_CACHE_MS`: process cache TTL in ms for full scans (default `1000`).
- `CONSENSUS_PROCESS_CACHE_FAST_MS`: process cache TTL in ms for fast scans (default `500`).
- `CONSENSUS_SESSION_CACHE_MS`: Codex session list cache TTL in ms for full scans (default `1000`).
Expand Down
90 changes: 85 additions & 5 deletions dev/dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const tsc = spawnChild("tsc", [
"--pretty",
"false",
]);
const server = spawnChild("server", [tsxPath, "watch", "src/server.ts"]);

const parsePort = (value) => {
const num = Number(value);
Expand All @@ -66,7 +65,79 @@ const basePort =
parsePort(process.env.VITE_PORT) ??
5173;

const startVite = async () => {
const baseServerPort = parsePort(process.env.CONSENSUS_PORT) ?? 8787;

const startServer = async () => {
const maxAttempts = 6;

for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const port = baseServerPort + attempt;
process.stderr.write(`[dev] starting server on port ${port}\n`);

const child = spawn(
process.execPath,
[tsxPath, "watch", "src/server.ts"],
{
env: { ...process.env, CONSENSUS_PORT: String(port) },
stdio: ["ignore", "pipe", "pipe"],
}
);

let output = "";
let running = false;
const onData = (chunk) => {
const text = chunk.toString();
output += text;
process.stdout.write(text);
};
child.stdout?.on("data", onData);
child.stderr?.on("data", onData);

child.on("exit", (code) => {
if (running) {
process.stderr.write(`[dev] server exited with ${code ?? 0}\n`);
shutdown(code ?? 1);
}
});

const result = await new Promise((resolve) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
running = true;
children.push(child);
resolve({ status: "running" });
}, 1500);

child.on("exit", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
const conflict = /EADDRINUSE|address already in use|port .* in use/i.test(
output
);
resolve({ status: conflict ? "conflict" : "exit", code });
});
});

if (result.status === "running") return port;
if (result.status === "conflict") {
process.stderr.write(`[dev] port ${port} in use, trying next\n`);
continue;
}

process.stderr.write("[dev] server exited unexpectedly\n");
shutdown(1);
return;
}

process.stderr.write("[dev] failed to find an open port for server\n");
shutdown(1);
return null;
};

const startVite = async (serverPort) => {
const maxAttempts = 6;

for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
Expand All @@ -78,7 +149,11 @@ const startVite = async () => {
[vitePath, "--port", String(port), "--host", "127.0.0.1", "--strictPort"],
{
cwd: path.join(root, "public"),
env: { ...process.env, CONSENSUS_UI_PORT: String(port) },
env: {
...process.env,
CONSENSUS_UI_PORT: String(port),
CONSENSUS_PORT: String(serverPort),
},
stdio: ["ignore", "pipe", "pipe"],
}
);
Expand Down Expand Up @@ -127,7 +202,13 @@ const startVite = async () => {
shutdown(1);
};

void startVite();
const main = async () => {
const serverPort = await startServer();
if (!serverPort) return;
await startVite(serverPort);
};

void main();

const shutdown = (code = 0) => {
for (const child of children) {
Expand All @@ -138,7 +219,6 @@ const shutdown = (code = 0) => {
};

tsc.on("exit", (code) => shutdown(code ?? 0));
server.on("exit", (code) => shutdown(code ?? 0));

process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));
11 changes: 10 additions & 1 deletion dev/tmux-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@
set -euo pipefail

SCOUT_DIR="$(pwd -P)"
exec env SCOUT_DIR="$SCOUT_DIR" tmux -f ./dev/tmux.conf new -A -s scout

if tmux has-session -t scout 2>/dev/null; then
tmux set-environment -g SCOUT_DIR "$SCOUT_DIR"
tmux set-environment -g PATH "$PATH"
tmux set-environment -t scout SCOUT_DIR "$SCOUT_DIR"
tmux set-environment -t scout PATH "$PATH"
exec tmux -f ./dev/tmux.conf attach -t scout
fi

exec env SCOUT_DIR="$SCOUT_DIR" PATH="$PATH" tmux -f ./dev/tmux.conf new -s scout
9 changes: 6 additions & 3 deletions dev/tmux.conf
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Keep PATH in sync between shell and tmux.
set -ga update-environment "PATH"

# Show useful window names automatically: "<command>:<directory>"
# This makes Ctrl+b w show both the agent (command) and the worktree (dir).
setw -g automatic-rename on
setw -g automatic-rename-format '#{pane_current_command}:#{b:pane_current_path}'

# New windows should always start in the default worktree ("scout directory" in the demo).
# We set SCOUT_DIR in the session start command (see next section).
bind c new-window -c "#{env:SCOUT_DIR}"
# New windows should start in the default worktree when SCOUT_DIR is set.
# Fallback to the current pane path if SCOUT_DIR is missing.
bind c new-window -c "#{?env:SCOUT_DIR,#{env:SCOUT_DIR},#{pane_current_path}}"
Loading
Loading