* fix(qa): ISSUE-001 mobile kanban columns
* Fix terminal tmux targeting for session detail views
* fix(web): unblock event loop in tmux-name resolution and drop dup CSS
- mux-websocket: switch resolveExactTmuxName from execFileSync to
promisified execFile so a slow tmux call no longer stalls the
WebSocket message handler. Propagate async through TerminalManager.open
/ subscribe and the pty.onExit reattach path.
- globals.css: remove duplicate `.kanban-board { grid-template-columns:
minmax(0, 1fr) }` rule. The 767px breakpoint already covers it.
Addresses Greptile feedback on PR #1608.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(web): drop defensive tmux-name precheck
The has-session precheck in resolveExactTmuxName was UX padding around
the actual fix (using tmux's `=` exact-match prefix). Without the
precheck:
- attach-session fails naturally on a stale tmux name
- the existing reattach + exit-notify path surfaces the failure
- open()/subscribe() can stay sync — no event-loop concern, no async
cascade through the WS message handler
Net: -64 / +21 in mux-websocket.ts. Reverts the integration test that
relied on the precheck to its pre-PR id-based form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real fix:
- events-db: add closeDb() to release the better-sqlite3 file lock on
activity-events.db. Without it, Windows callers cannot rmSync the AO
base dir while the connection is open. Test teardowns in
test-utils.ts and plugin-integration.test.ts now call closeDb()
before rm to fix 4 EBUSY failures.
Test-only:
- tmux-utils.test.ts: normalize backslashes to forward slashes in two
resolveTmuxSession 'hash-prefix' tests; matches the pattern already
used by sibling tests in the same file.
- dashboard.test.ts, script-runner.test.ts, update-script.test.ts:
add it.skipIf(process.platform === 'win32') to four tests that
assert Unix-specific behavior (lsof cwd matching, posix script
paths, ao-update.sh smoke). Each file already uses the same skip
pattern for sibling tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Windows pipe relay was project-scoped only on outbound WS frames.
Server-side storage and pipe-path resolution still keyed by bare session
id, so two projects sharing a session id on the same mux connection
would collide on the same socket/buffer entry, and resolvePipePath
returned the first matching project's metadata regardless of caller
intent. Brings the Windows path in line with the Unix subscriptionKey
contract.
- resolvePipePath(sessionId, projectId?, fs?) reads only the caller's
project metadata when projectId is provided; legacy callers keep the
walk-all-projects fallback.
- winPipes / winPipeBuffers keyed by \${projectId}:\${id}.
- projectId threaded through handleWindowsPipeMessage data/resize/close
cast sites.
- Tests cover the project-collision case in both mux-websocket and
tmux-utils.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(web): remove SSE entirely — browser uses WebSocket only
- Delete GET /api/events route (no consumers remain)
- Refactor SessionBroadcaster: replaces SSE stream fetch with a plain
setInterval polling GET /api/sessions/patches every 3s, eliminating
the last server-side SSE consumer
- Remove EventSource from useSessionEvents; hook is now WebSocket-only
via mux.sessions; rename SSEAttentionMap → AttentionMap and
sseAttentionLevels → attentionLevels throughout
- Replace useSSESessionActivity with useMuxSessionActivity — thin
selector over useMux().sessions, no network call
- Delete SSESnapshotEvent and SSEActivityEvent types from lib/types.ts
- Delete Dashboard.renderCadence.test.tsx (SSE-specific test)
- Update ARCHITECTURE.md to reflect the simplified two-protocol design
(HTTP + WebSocket only; no SSE anywhere in the system)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(web): address PR review comments
- useMuxSessionActivity: switch to useMuxOptional (consistent with page.tsx),
add useMemo for referential stability
- useSessionEvents: validate patch.status against VALID_SESSION_STATUSES before
casting; type all three VALID_* sets with satisfies for exhaustiveness
- mux-websocket: remove leading underscores from private fields (intervalId,
polling) — private modifier already conveys intent
- Dashboard.renderCadence test: port from SSE/EventSource to MuxProvider mock;
covers same-membership-snapshot-only-rerenders-changed-card invariant
- Remove .feature-plans/pending/remove-browser-sse.md (duplicated in PR body)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): guard broadcast against stale fetch after disconnect
If disconnect() runs while fetchSnapshot() is in flight, the .then
callback would still fire broadcast() into an empty (or re-populated)
subscriber set. Guard with intervalId !== null so stale resolutions
after the last subscriber leaves are silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): remove SSE-specific tests from emptyState suite
The upstream added two tests for live load-error banners driven by SSE
onmessage events. Since this PR removes SSE entirely, those tests can't
pass and the SSE mock setup is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): restore liveSessionsResolved to prevent premature banner dismiss
mux?.status === "connected" fires on WebSocket handshake before any
session data arrives. In the SSR-failure scenario (dashboardLoadError
set), this was dismissing the error banner as soon as the WS opened,
leaving users with a silent empty dashboard.
Restore liveSessionsResolved: set it only from the first successful
HTTP /api/sessions refresh or mux snapshot (same semantics as main).
The reset action from the initialSessions effect intentionally does
not set it (liveResolved flag absent = SSR-only reset, not live data).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): port live load-error banner from SSE to WS transport
SessionBroadcaster now emits { ch: "sessions", type: "error" } on fetch
failure instead of silently returning null. MuxProvider surfaces the error
as lastError on the context. useSessionEvents restores loadError reducer
state, synced from muxLastError, cleared on successful snapshot or HTTP
refresh. Dashboard renders the live error banner via loadError ?? ssrLoadError.
Two emptyState tests ported to drive errors through MuxProvider mock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Gaurav Bhola <fastestdevalive@users.noreply.github.com>
Four small fixes flagged in review of the Windows port:
- core/platform.ts: AO_SHELL override now infers args flag from the shell
basename (cmd → /c, bash/sh/zsh → -c, anything else → -Command). Previously
every override got PowerShell args, so AO_SHELL=cmd or AO_SHELL=bash
silently broke run-command flows.
- core/global-config.ts: defaults.runtime now resolves to getDefaultRuntime()
(process on Windows, tmux elsewhere) instead of the hardcoded "tmux".
First-run on Windows no longer writes a config that immediately fails
runtime resolution.
- web/server/mux-websocket.ts: validateSessionId now runs on the Windows
named-pipe relay path. The Unix branch validates inside TerminalManager;
the Windows path bypassed it entirely, so an unsanitised id became both
a map key and was interpolated into a pipe path downstream.
- web/server/tmux-utils.ts: resolvePipePath now reads the V2 JSON layout
(~/.agent-orchestrator/projects/{projectId}/sessions/{id}.json) first,
then falls back to V1 line-delimited metadata for users who haven't run
ao migrate-storage. The single-source-of-truth note is still accurate;
the search just covers both layouts during the migration window.
Each change has a paired unit test.
Resolve tmux-utils.ts conflict: rewrite resolvePipePath to read
runtimeHandle.data.pipePath from on-disk session metadata instead of
recomputing a hash from AO_CONFIG_PATH. Mirrors the storageKey
disambiguation pattern upstream introduced for resolveTmuxSession in
PR #1488 (eca3001c). Recomputing the hash drifted from the runtime's
deriveStorageKey on Windows (raw backslash paths vs POSIX-normalized),
so the dashboard's named-pipe relay was connecting to the wrong path
and falling back to ENOENT while ao session attach worked fine.
Also normalize path separators in 6 upstream resolveTmuxSession tests
that hard-coded Unix forward slashes in their exists() mocks; on
Windows path.join produces backslashes and the assertions never
matched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): resolve tmux sessions when storageKey is wrapped ({hash}-{projectName})
ao-core names tmux sessions as `{storageKey}-{sessionId}`, where
storageKey can be either a bare 12-hex hash or the legacy wrapped form
`{hash}-{projectName}`. The resolver only handled the bare-hash form, so
the dashboard terminal never attached for any project whose
storageKey includes the project-name segment (the default on 0.3.0).
Fix (closes#1486): resolve the owning storageKey from disk via
`~/.agent-orchestrator/{storageKey}/sessions/{sessionId}`, then ask tmux
for the exact `{storageKey}-{sessionId}` name via `has-session -t =`.
The on-disk record is authoritative, which avoids ambiguous suffix
matches (e.g. looking up `app-1` must not resolve a distinct session
`{hash}-my-app-1`). If multiple storageKeys own the sessionId (rare,
same-sessionId across projects), each candidate is probed so a stale
metadata dir can't shadow the live session of another project. The
tmux-list-sessions fallback still recovers bare-hash sessions when the
on-disk record is absent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(web): filter readdir to directories only in tmux resolver
readdirSync without withFileTypes includes plain files, so a stray
file matching STORAGE_KEY_PATTERN (e.g. a backup file named
`aabbccddeef0`) would trigger an unnecessary existsSync probe.
Harmless for correctness but an avoidable syscall per attach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): collapse attention zones to 4 with feature flag for 5 (#1201)
Simplify the dashboard to a 4-zone kanban by default (WORKING,
PENDING, ACTION, READY), merging the former REVIEW + RESPOND
columns into a single ACTION zone that just asks "does the human
need to do something?". The card-level badges still surface the
underlying granular state (ci_failed, needs_input, changes_requested),
so nothing is lost — it just moves off the column header.
Add a `dashboard.attentionZones` config (defaults to "simple") that
opts back into the original 5-zone layout for power users. The
function-level `getAttentionLevel(session, mode)` still defaults to
"detailed" so card-level call sites (SessionCard, BottomSheet,
ProjectSidebar, etc.) keep their granular behavior untouched. Only
the Dashboard kanban and SSE server routes read the config and pass
the mode.
Closes#1201
* fix(web): address PR review feedback on attention zones (#1201)
- Drop .strict() from DashboardConfigSchema (Cursor Bugbot):
matches other schemas in config.ts so typos in the dashboard
block gracefully default instead of crashing the whole loader.
- DynamicFavicon: keep `action` at yellow, not red (Codex P2).
In simple mode, `action` collapses respond + review, which
means it necessarily catches routine review work (ci_failed,
changes_requested) that was previously yellow. Escalating to
red would make every typical PR scream critical. Only the
detailed-mode `respond` bucket still triggers red.
- useSessionEvents default → "detailed" (Codex P3). The hook
default now matches getAttentionLevel's default so callers
that seed with `getAttentionLevel(s)` (no mode) — notably
PullRequestsPage — stay consistent with their seed after
the first live SSE/refresh snapshot. Dashboard explicitly
passes the config's attentionZones to opt into simple mode.
- Expand mobile action chip (Codex P3). When a session
collapses to `action`, derive the most specific underlying
cause (needs input, stuck, errored, ci failed, changes,
waiting, conflicts) instead of falling through to the
generic "action" label, so mobile users keep the reason to
intervene.
- DynamicFavicon tests: add cases for `action` staying yellow
and `respond` still escalating when both are present.
* fix(web): thread attentionZones config through PullRequestsPage (#1201)
Cursor Bugbot caught a deeper version of the same inconsistency
my previous fix didn't fully resolve: the server SSE route uses
`config.dashboard?.attentionZones ?? "simple"`, but PullRequestsPage
was seeding `initialAttentionLevels` and calling useSessionEvents
without passing any mode (falling back to the hook's "detailed"
default). Result: snapshots from the server arrived as "action"/
"merge", then scheduleRefresh recomputed them client-side as
"respond"/"review", and the favicon oscillated between yellow
and red on every refresh cycle.
Fix: plumb pageData.attentionZones through prs/page.tsx into
PullRequestsPage, use it in both the seed and useSessionEvents.
Now the seed, the server SSE, and the client refresh all use the
same mode — stable, no flicker.
* fix(web): accurate action labels for crashed agents + yellow tone (#1201)
Two Cursor Bugbot findings on the action-zone collapse work:
1. [Medium] Mobile chip mislabeled crashed agents as "needs input".
When an agent's activity is `exited` and the session is in the
action bucket, the agent has crashed — sending it a message
won't help, the user needs to restart. Split the label: exited
→ "crashed", blocked → "blocked".
2. [Low] ProjectMetric for Action used tone="error" (red), which
contradicts the favicon fix's rationale. The action bucket
catches routine review work (ci_failed, changes_requested)
that was previously orange/yellow severity; screaming red in
the project overview grid would have the same cry-wolf problem
as the favicon did. Use tone="orange" (the less severe of the
two merged buckets) to match.
* refactor(web): make attentionZones required in useSessionEvents (#1201)
Cursor Bugbot caught that the hook default of "detailed" mismatches
the server SSE route default of "simple" — a latent footgun for any
future caller that forgets to pass the mode explicitly. The exact
oscillation bug I already fixed once for PullRequestsPage would
silently come back on the next page that uses this hook.
Real fix: refactor the hook to take an options object and make
`attentionZones` required with no default. TypeScript now enforces
that every caller explicitly passes the mode the server is using.
You literally cannot call useSessionEvents without supplying it.
Also cleaner: the hook had 6 positional parameters, which is past
the point where an options object helps readability. Dashboard and
PullRequestsPage call sites now read as self-documenting.
* fix(web): address human code review on attention zones (#1201)
Review from i-trytoohard on commit 8d27db79:
1. [Medium] `ZoneCounts` in sessions/[id]/page.tsx had an `action`
field that was always 0 — the page always computes in detailed
mode (getAttentionLevel with no mode = detailed), and the
consumer (`OrchestratorZones` in SessionDetail) never reads
an `action` field either. Removed the dead field, added a
`continue` guard for the never-reached "action" case so
TypeScript narrows the index correctly, and documented the
intent in a header comment.
2. [Medium] The simple-mode action chip had 10+ sub-conditions
with no tests. Extracted the label logic into a pure
`getActionChipLabel(session)` helper and added 16 unit tests
covering every branch: status-based signals (needs_input,
stuck, errored, ci_failed, changes_requested), activity-based
(waiting_input, exited→crashed, blocked), PR-based (failing
CI, changes_requested, conflicts), precedence between signal
classes, and the generic fallback.
3. [Low] `LEVEL_LABELS.action` in ProjectSidebar is dead code
today (sidebar uses detailed mode). Added a comment explaining
why the entry still exists (TypeScript exhaustiveness on
`Record<AttentionLevel, string>` + forward-compat).
* fix(web): address inline review on attention zones (#1201)
- Reorder getActionChipLabel to mirror getDetailedAttentionLevel: respond-class
activity (waiting_input/exited/blocked) now outranks review-class status
(ci_failed/changes_requested). A crashed agent with changes_requested no
longer reads as "changes" and hides the crash.
- Paint data-level="action" dot with --color-accent-orange to match the
favicon's yellow-severity treatment of the collapsed bucket.
- Tighten attentionLevel on the mux boundary (mux-protocol, mux-websocket,
useSessionEvents) from string to AttentionLevel so invalid values like "none"
no longer launder through into DynamicFavicon's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- Fix CI diff coverage and review feedback
- Fix test reliability for session attach, stop, and Windows attach
- Add coverage for session attach binary protocol and edge cases
- Clean up stdin listener + add resolvePipePath tests
- Address review comments + coverage for pipe relay
- Make 80 failing tests pass on Windows (cross-platform mocks, path assertions)
- Fix production code: execFile shell option for .cmd, isPathInside separator,
openclaw binary detection via `where` on Windows
- Add platform-aware test assertions for shell escaping, PATH handling, hooks
- Add signal forwarding comment for Windows dashboard process
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows equivalent of the tmux daemon. Per-session detached pty-host.js
process owns a ConPTY (via node-pty), listens on a named pipe
(\.\pipe\ao-pty-{hash}-{sessionId}), and relays terminal I/O to any
connected client.
- runtime-process: spawn pty-host on Windows, route sendMessage/getOutput/
isAlive/destroy through named pipe protocol
- mux-websocket: named pipe relay for dashboard terminal (skip TerminalManager
on Windows), resolvePipePath via generateConfigHash (instant, no pipe scan)
- direct-terminal-ws: use real mux server on Windows instead of placeholder
- tmux-utils: findTmux returns null on Windows, resolvePipePath added
- orchestrator-prompt: runtime-agnostic language
- opencode: fix isProcessRunning tmux-before-guard bug (W29)
Addresses blockers W01-W12, W23, W24, W28, W29, W33-W35.
Unix behavior completely unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renames all npm package scopes from @composio/* to @aoagents/* and
updates GitHub repo references from ComposioHQ/agent-orchestrator
to aoagents/ao throughout the codebase.
- All package.json names and dependencies
- README badges, links, and install instructions
- Documentation references
- Changeset config
- Source code imports and test files
- Export SessionBroadcaster for testing
- Add 10 tests covering subscribe, broadcast, fetchSnapshot, SSE
reconnection, disconnect, and error isolation
- Covers the major uncovered lines in mux-websocket.ts (66-194)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace three separate real-time channels (per-terminal WS, SSE, HTTP poll)
with a single persistent multiplexed WebSocket at /mux.
Architecture:
- Browser ↔ MuxProvider owns one WS connection per tab (/mux)
- Terminal I/O, resize, open/close all flow over mux channels
- Session status patches delivered via a shared SSE relay:
mux server subscribes once to Next.js /api/events (SSE) and
broadcasts to all connected browser clients — no per-client polling
- Manual WS upgrade routing fixes ws library limitation with multiple
WebSocketServer instances on the same HTTP server
Remove:
- terminal-websocket.ts (legacy ttyd-based per-session server, port 14800)
- Per-terminal WebSocket connections from DirectTerminal
- Per-client 5 s HTTP polling for session patches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add end-to-end observability across core, web, and terminal
Instrument lifecycle/API/websocket flows with correlation-aware metrics and operator health surfaces so the system can self-diagnose and escalate failures.
* fix: realign session restore set and unblock claim PR typecheck
* fix(web): restore project-filtered sessions route after main merge
* fix(core): remove unused orchestrator import to unblock lint
* fix(core): always record lifecycle poll failures and remove dead review branches
* fix(web): harden websocket metrics and reuse SSE observers
* fix(web): preserve primary session API errors when services bootstrap fails
* fix(web): use full orchestrator config type in SSE observer helper
* fix(web): restore project-scoped SSE and guard observability error paths
* fix(web): address remaining Bugbot review gaps
* fix(web): align SSE project attribution and share session project resolver
* fix(web): require request arg for SSE route and align tests
* fix(web): record websocket error disconnects as failures
* fix(web): use path alias for session project resolver import
* fix(web): include active connection count in disconnect metrics
* fix: terminal servers compatible with hash-based architecture
The terminal WebSocket servers (direct-terminal-ws and terminal-websocket)
used config.dataDir to validate sessions, which no longer exists in the
hash-based architecture. Also fixed node-pty failing to find tmux via
posix_spawnp.
Changes:
- Remove config.dataDir dependency, validate via `tmux has-session` instead
- Add resolveTmuxSession() to map user-facing IDs (ao-15) to hash-prefixed
tmux names (8474d6f29887-ao-15)
- Use explicit tmux path discovery (findTmux) since node-pty's posix_spawnp
doesn't reliably inherit PATH
- Include /opt/homebrew/bin in fallback PATH for macOS ARM
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add session resolution to ttyd server and use exact tmux matching
- Add findTmux() and resolveTmuxSession() to terminal-websocket.ts
(previously only in direct-terminal-ws.ts), fixing hash-prefixed
session lookup for the ttyd-based terminal server
- Use tmux exact match prefix (=sessionId) in has-session checks
to prevent ao-1 from matching ao-15 via prefix matching
- Add server compatibility tests that verify both servers handle
hash-based architecture correctly (14 tests)
- Include server/ in tsconfig and vitest config for typecheck coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract tmux-utils and add proper unit tests
- Extract findTmux(), resolveTmuxSession(), validateSessionId() into
shared server/tmux-utils.ts — eliminates duplication between
direct-terminal-ws.ts and terminal-websocket.ts
- Add 20 real unit tests with injected mocks that test actual behavior:
- findTmux: candidate priority, fallback to bare name
- resolveTmuxSession: exact match, hash-prefix resolution,
= prefix for preventing tmux prefix matching (ao-1 vs ao-15),
null when no session found, tmux not running
- validateSessionId: path traversal, shell injection, whitespace
- Slim down server-compatibility.test.ts to 10 structural checks
(imports tmux-utils, no loadConfig, no config.dataDir, no existsSync)
Total: 30 tests — all pass on fix branch, 8 fail on main
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add real integration tests for direct-terminal-ws
- Refactor direct-terminal-ws to export createDirectTerminalServer()
factory so tests can control server lifecycle without side effects
- Add 10 integration tests that create real tmux sessions, start the
real server, connect via WebSocket, and verify the full flow:
- Health endpoint returns 200
- Missing session parameter → close 1008
- Path traversal in session ID → close 1008
- Shell injection in session ID → close 1008
- Nonexistent tmux session → close 1008
- Real tmux session → connects and receives terminal output
- Hash-prefixed session resolution works end-to-end
- Can send input and receive echoed output
- Resize messages don't crash the connection
- Unknown HTTP path → 404
- Tests create/destroy tmux sessions in beforeAll/afterAll
- Server runs on random port (port 0) to avoid conflicts
- Total test suite: 40 tests (20 unit + 10 compatibility + 10 integration)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: add web server tests to CI pipeline
The web package was explicitly excluded from CI test runs
(pnpm -r --filter '!@composio/ao-web' test). Add a test-web job
that installs tmux, starts the tmux server, and runs the web
package tests (unit + integration).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address CI failures and bugbot review comments
- Fix lint: replace require() with ESM import in integration test
- Fix base-path mismatch: ttyd now uses user-facing sessionId for
--base-path/URL and actual tmux name for attach-session
- Fix bare "tmux" in ttyd spawn args: use TMUX constant (full path)
- Scope CI test-web job to server/__tests__/ to avoid pre-existing
failures in src/__tests__/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: comprehensive unit and integration test coverage
Unit tests (77): validateSessionId covers all injection vectors (shell,
path traversal, command substitution, special chars, unicode, control
chars), findTmux covers all candidate paths and error types,
resolveTmuxSession covers exact match, hash-prefix resolution, suffix
matching precision, edge cases (single char, long lists, multiple
hyphens, different tmux paths).
Integration tests (45): health endpoint lifecycle (active count tracks
connections/disconnections), HTTP routing (404s for all non-health
paths), WebSocket validation (11 injection/traversal vectors), terminal
connection (resize, multi-resize, invalid JSON, non-resize JSON),
hash-prefixed resolution (suffix match, command passthrough, session key
tracking, cross-match prevention), terminal I/O (Ctrl-C, Tab, Enter,
empty messages, rapid keystrokes, multi-line), connection lifecycle
(cleanup, rapid connect/disconnect, error recovery), server creation
(independent instances).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate 12-char hex prefix in hash-prefixed session resolution
The previous endsWith("-{sessionId}") suffix match was ambiguous:
"hash-my-app-1" would falsely match a lookup for "app-1". Now validates
that the prefix matches the exact format generated by generateConfigHash
(12-char lowercase hex) before comparing the remainder.
Added unit tests for the ambiguity case and invalid prefix formats.
Updated integration test session names to use proper 12-char hex prefixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>