Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
c5dbc2c
Add TypeAgent Shell VS Code extension scaffold
TalZaccai Apr 21, 2026
ef3da8f
Fix WebSocket RPC protocol for agent server connection
TalZaccai Apr 21, 2026
b4c5ece
Fix chat display: proper DisplayContent rendering, message dedup, and…
TalZaccai Apr 21, 2026
3e1f286
Add session management commands
TalZaccai Apr 21, 2026
ece0c5f
Fix session switch channel error and prevent duplicate session names
TalZaccai Apr 21, 2026
48f4b8f
Merge remote-tracking branch 'origin/main' into talzacc/vscode-shell-…
TalZaccai Apr 21, 2026
02cd582
Rename session to conversation in user-facing text
TalZaccai Apr 21, 2026
14fb03e
Fix slow conversation switch and suppress disconnect error
TalZaccai Apr 21, 2026
ff8e7b3
Update default agent server port from 3000 to 8999
TalZaccai Apr 21, 2026
b7c51db
Show 'Switching conversation...' state and disable input during switch
TalZaccai Apr 21, 2026
f28d531
Add conversation history replay and adopt join-before-leave switch
TalZaccai Apr 22, 2026
3edcd4b
Replace history separators with per-bubble timestamps
TalZaccai Apr 22, 2026
22ae986
Batch history replay into single atomic message
TalZaccai Apr 22, 2026
c498301
Don't replay history on websocket reconnect
TalZaccai Apr 22, 2026
9e0ff06
Render ANSI escape codes and markdown in chat output
TalZaccai Apr 22, 2026
919bd94
Improve agent bubble + ANSI color contrast
TalZaccai Apr 22, 2026
94c51eb
Don't prefer HTML alternates with hardcoded light-theme colors
TalZaccai Apr 22, 2026
7ebeb14
Use HTML alternates but strip hard-coded inline colors
TalZaccai Apr 22, 2026
9943898
Restyle chat to match shell layout: avatars + header above bubble
TalZaccai Apr 22, 2026
7701a2b
Add user status icons, real username, and per-agent emojis
TalZaccai Apr 22, 2026
027bc46
Thread requestId through send/complete to mark correct user bubble
TalZaccai Apr 22, 2026
e86793a
Replace user-bubble checkmark with shell-style roadrunner icon
TalZaccai Apr 22, 2026
8e96891
Fix roadrunner: extract clientRequestId from RequestId object
TalZaccai Apr 23, 2026
e41e097
Hide roadrunner until 'explained' notify arrives
TalZaccai Apr 23, 2026
9ea1ba9
Always show roadrunner; update tooltip when no translation occurs
TalZaccai Apr 23, 2026
b739bc9
Revert "Always show roadrunner; update tooltip when no translation oc…
TalZaccai Apr 23, 2026
48fe0f9
shell-chat: hover timing on bubbles + clickable action JSON
TalZaccai Apr 23, 2026
75cbf46
shell-chat: inline shell-style metrics row + JSON syntax highlighting
TalZaccai Apr 23, 2026
325cba4
shell-chat: only show timing on bubble hover; brighter JSON tokens
TalZaccai Apr 23, 2026
4bfdc36
shell-chat: shell-style metrics footer with separator + tinted bg
TalZaccai Apr 23, 2026
331a53a
shell-chat: replay set-display-info + thread requestId; metrics on us…
TalZaccai Apr 23, 2026
76076c5
dispatcher: persist command-result + setDisplayInfo entries in Displa…
TalZaccai Apr 23, 2026
db8abe9
shell-chat: split user vs agent bubble timing to match shell
TalZaccai Apr 23, 2026
e46526e
feat(typeagent-shell): hide empty agent bubble + multi-chat side panels
TalZaccai Apr 24, 2026
5384dcd
feat(typeagent-shell): active-chat tracking + friendlier panel names
TalZaccai Apr 24, 2026
585c386
fix(typeagent-shell): track session name through switch/rename + drop…
TalZaccai Apr 24, 2026
903c5e5
fix(webSocketChannelServer): drop sends to non-open sockets to preven…
TalZaccai Apr 24, 2026
cbfde20
feat(typeagent-shell): persist ephemeral panel chats once user engages
TalZaccai Apr 24, 2026
5677f82
feat(typeagent-shell): add Clear Chat command + default keybindings
TalZaccai Apr 24, 2026
5b3c36d
fix(typeagent-shell): use chord keybindings gated on chat focus
TalZaccai Apr 24, 2026
63415d3
chore(typeagent-shell): use Ctrl+K Ctrl+T for New Chat (Side Panel)
TalZaccai Apr 24, 2026
1ed51f8
fix(typeagent-shell): drive keybinding when-clauses from a real focus…
TalZaccai Apr 24, 2026
08bc719
feat(typeagent-shell): mute connection ribbon for inactive chats
TalZaccai Apr 24, 2026
427f8fd
feat(typeagent-shell): persist chat panels across VS Code reload
TalZaccai Apr 24, 2026
aeeeded
feat(typeagent-shell): loading-history input lock, global shortcuts, …
TalZaccai Apr 24, 2026
64d0a5f
fix(typeagent-shell): open new chat panels in next column to keep the…
TalZaccai Apr 24, 2026
32bb652
chore(typeagent-shell): tighten .vscodeignore for vsix packaging
TalZaccai Apr 24, 2026
2cd7ed7
fix(typeagent-shell): join sessions unfiltered so duplicate tabs both…
TalZaccai Apr 24, 2026
c95a550
fix(typeagent-shell): render user messages from other tabs on shared …
TalZaccai Apr 24, 2026
3d1685e
fix(typeagent-shell): dedup setDisplay+appendDisplay and drop late temps
TalZaccai Apr 24, 2026
e5fcc00
fix(typeagent-shell): defensive append dedup and forward metrics to p…
TalZaccai Apr 24, 2026
10eb451
fix(websocket-channel-server): always handle ws.send errors so a dead…
TalZaccai Apr 24, 2026
50d2ddd
fix(typeagent-shell): drop all temporary status messages after comman…
TalZaccai Apr 24, 2026
73f707a
feat(code,typeagent-shell): chat-driven conversation actions
TalZaccai Apr 24, 2026
3a18755
feat(code): default-enable code-typeagent-shell sub-schema
TalZaccai Apr 24, 2026
737c9ba
fix(typeagent-shell): clear temp when permanent lands in empty bubble
TalZaccai Apr 24, 2026
cb8ddab
fix(typeagent-shell): peer tab clears temp + engages late-guard on me…
TalZaccai Apr 24, 2026
a9daced
feat(code): strengthen typeagent-shell action descriptions for transl…
TalZaccai Apr 24, 2026
9d881a3
fix(code): return ActionResult from typeagent-shell handlers
TalZaccai Apr 24, 2026
2c038d0
feat(completion-ui): extract dropdown menu + toggle to shared package
TalZaccai Apr 25, 2026
45f82d0
feat(typeagent-shell): add IntelliSense completion to chat input
TalZaccai Apr 25, 2026
71abcb7
fix(typeagent-shell): polish IntelliSense UI + theme + add robot icon
TalZaccai Apr 25, 2026
4744a8d
refactor(vscode-shell): rename typeagent-shell to vscode-shell + move…
TalZaccai Apr 27, 2026
0b015d0
docs(vscode-shell): add README with install + usage instructions
TalZaccai Apr 27, 2026
5333d82
fix(vscode-shell): session restore + peer command-complete cleanup
TalZaccai Apr 27, 2026
9743a06
fix(dispatcher): make reasoning engine configurable + tolerate dotted…
TalZaccai Apr 28, 2026
19b44cd
fix(agents): tighten action descriptions + fix coda newFile to write …
TalZaccai Apr 28, 2026
539e882
feat(vscode-shell): natural-keystroke demo runner + per-line timeout …
TalZaccai Apr 28, 2026
9bdfee5
feat(chat-ui): thread requestId through onSend/addUserMessage/injectC…
TalZaccai Apr 28, 2026
2ec09ca
feat(chat-ui): requestId-keyed agent bubble routing
TalZaccai Apr 28, 2026
23ae01c
feat(chat-ui): add typeAndSend(text, requestId) for natural keystroke…
TalZaccai Apr 28, 2026
2f2e0a4
feat(chat-ui): add setSwitching + setHistoryLoading state coordination
TalZaccai Apr 28, 2026
f052f55
feat(chat-ui): add addSystemMessage + source-keyed avatar map
TalZaccai Apr 28, 2026
dee600c
Merge remote-tracking branch 'origin/main' into talzacc/vscode-shell-…
TalZaccai Apr 28, 2026
9a3243b
chat-ui(A6): replayHistory(entries) for session-history transcript re…
TalZaccai Apr 28, 2026
cee9dc0
chat-ui(A8): demo pause hook with key capture + banner
TalZaccai Apr 28, 2026
6eceff3
chat-ui: requestId-target setDisplayInfo/completeRequest + add hasUse…
TalZaccai Apr 28, 2026
4748d54
vscode-shell: swap legacy ChatUI for shared chat-ui ChatPanel (B1+B3)
TalZaccai Apr 29, 2026
de77cf2
vscode-shell: normalize RequestId at the bridge boundary (B2)
TalZaccai Apr 29, 2026
442433e
chat-ui: polish hover/metrics/JSON, persist tokens & first-message in…
TalZaccai Apr 30, 2026
7569473
chat-ui: port partialCompletion + setUserInfo for vscode-shell parity
TalZaccai Apr 30, 2026
fa884b5
vscode-shell: regenerate pnpm-lock.yaml to include vscode-shell + com…
TalZaccai Apr 30, 2026
41ac93d
vscode-shell/chat-ui: address code-review findings
TalZaccai Apr 30, 2026
1f12cb8
vscode-shell: fix three race conditions surfaced by code review
TalZaccai Apr 30, 2026
3c9a521
fix(vscode-shell): demo cancel, stop button, ghost-text polish
TalZaccai May 1, 2026
2dd7240
feat(vscode-shell): single in-place reconnect countdown
TalZaccai May 1, 2026
d3188e5
style: prettier formatting
TalZaccai May 1, 2026
a2a8d1d
fix: code-review findings batch
TalZaccai May 1, 2026
20e0dc7
feat(dispatcher): cross-schema hint in copilot/claude reasoning
TalZaccai May 2, 2026
3fc9f28
merge: origin/main (PR #2277 IntelliSense + demo polish)
TalZaccai May 2, 2026
0ea9985
fix(vscode-shell): close demo startup race with sync demoStarting flag
TalZaccai May 4, 2026
e8536bc
feat(chat-ui): port Tab/Enter completion behavior from PR #2277
TalZaccai May 4, 2026
88bd194
Merge remote-tracking branch 'origin/main' into talzacc/vscode-shell-…
TalZaccai May 4, 2026
689acf8
fix: regenerate pnpm-lock.yaml for completionUI package
TalZaccai May 4, 2026
6365e09
fix: address PR #2291 review comments and CI policy failures
TalZaccai May 4, 2026
f2be6b5
Merge branch 'main' into talzacc/vscode-shell-chat
TalZaccai May 4, 2026
e272574
fix: rewrite highlightJson as hand-rolled scanner; fix prettier failures
TalZaccai May 4, 2026
3c613b1
fix: prettier formatting on completionUI/sharedDispatcher; allow vsce…
TalZaccai May 5, 2026
c2e9b42
fix: prettier on shell files; nicer instance-lock error; recover serv…
TalZaccai May 5, 2026
cf3a314
fix(vscode-shell): adapt to agent-server-client *Session -> *Conversa…
TalZaccai May 5, 2026
1828e8b
fix(agent-server): reject duplicate conversation names; race-protect …
TalZaccai May 5, 2026
cfe4f9a
fix(agent-server): prettier conversationManager.ts
TalZaccai May 5, 2026
ed9954e
fix(chat-ui): address CodeQL XSS findings; scope roadrunner hover to …
TalZaccai May 5, 2026
02e40bf
Fix shell->extension user-message mirror by clearing userMessageById …
May 5, 2026
d573b38
Fix demo regressions: clear stale ghost hint + focus on pause; per-la…
TalZaccai May 5, 2026
2b7b18c
vscode-shell: switch demo continue from Ctrl+Right to Alt+Right
TalZaccai May 5, 2026
f3b6aa9
shell: persist and restore last open conversation across launches
TalZaccai May 5, 2026
1624614
shell: reconnect on transient WebSocket disconnects instead of quitting
TalZaccai May 5, 2026
65b0067
shell: action JSON expands beneath message instead of overlaying it
TalZaccai May 5, 2026
a2e0ba8
shell: add reconnect banner + don't crash on stop-while-disconnected
TalZaccai May 5, 2026
9441dc9
shell: catch fire-and-forget dispatcher rejections + global guard
TalZaccai May 5, 2026
27c5a0c
shell: don't show stop button for mirrored peer commands
TalZaccai May 5, 2026
582b2e9
shell: consolidate dispatcher 'Executing action' status into agent bu…
TalZaccai May 5, 2026
99b74d0
shell: prettier formatting for instance.ts
TalZaccai May 5, 2026
3247a5c
chat-ui: preserve agent source/icon when bubble starts as dispatcher …
TalZaccai May 5, 2026
ad3ece7
docs: add chat-ui README; remove duplicate Trademarks section in vsco…
TalZaccai May 5, 2026
22d9be0
vscode-shell: synchronously clear timers + cap replayBuffer (review f…
TalZaccai May 5, 2026
cd4a6cb
vscode-shell + code: address PR review findings
TalZaccai May 5, 2026
da1f9c9
Address remaining PR review comments
TalZaccai May 5, 2026
feb94bb
Merge branch 'main' into talzacc/vscode-shell-chat
TalZaccai May 5, 2026
d7cc4fe
vscode-shell webview: forward sourceIcon from agent message
TalZaccai May 5, 2026
5229613
PR review polish: restore inline schema comments, clarify Coda WS com…
TalZaccai May 5, 2026
72f5346
completion-ui: invert dependency direction with agent-dispatcher (F3)
TalZaccai May 5, 2026
a4f99a6
visualStudio agent: shared bridge with refcount (fix EADDRINUSE on po…
TalZaccai May 6, 2026
9b87a6b
Merge branch 'main' into talzacc/vscode-shell-chat
TalZaccai May 6, 2026
a4ac7d6
Address Rob's PR comments (mechanical fixes)
TalZaccai May 6, 2026
a56b59d
Match Rob's @deprecated wording style on EditorActionCreateFile
TalZaccai May 6, 2026
1c7f1ba
code agent: route conversation actions by schemaName, not actionName …
TalZaccai May 6, 2026
a98a82e
code agent: type executeConversationAction with VSCodeConversationAct…
TalZaccai May 6, 2026
958d56d
Address Rob's PR comments: clean up prompt-wall schema descriptions
TalZaccai May 6, 2026
0e614b7
refactor(code): split NewFileAction by file type per Rob's schema gui…
TalZaccai May 6, 2026
ba386b0
refactor: strip schema prompt walls per Rob's best-practices
TalZaccai May 6, 2026
027ac85
revert(dispatcher): remove schema-collision workarounds
TalZaccai May 6, 2026
d79a3a9
merge: resolve conflict with origin/main + drop github-cli prompt walls
TalZaccai May 6, 2026
af67ceb
code+coda: drop newFile wire-name shim, propagate the schema split
TalZaccai May 6, 2026
2155083
displayLog: drop dead JSON.parse(JSON.stringify) clone of metrics
TalZaccai May 6, 2026
da5b156
Merge branch 'main' into talzacc/vscode-shell-chat
TalZaccai May 6, 2026
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
96 changes: 81 additions & 15 deletions dotnet/autoShell/Services/WindowsWindowService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class WindowsWindowService : IWindowService
private const uint WM_SYSCOMMAND = 0x112;
private const uint SC_MAXIMIZE = 0xF030;
private const uint SC_MINIMIZE = 0xF020;
private const uint SW_RESTORE = 9;

private struct RECT
{
Expand All @@ -30,40 +31,55 @@ private struct RECT
public int Bottom;
}

private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

[DllImport(NativeDlls.User32, SetLastError = true)]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

[DllImport(NativeDlls.User32)]
private static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect);
private static extern bool BringWindowToTop(IntPtr hWnd);

[DllImport(NativeDlls.User32)]
private static extern IntPtr GetDesktopWindow();
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

[DllImport(NativeDlls.User32)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName);

[DllImport(NativeDlls.User32, EntryPoint = "SendMessage", SetLastError = true)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, uint wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();

[DllImport(NativeDlls.User32)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
private static extern IntPtr GetDesktopWindow();

[DllImport(NativeDlls.User32)]
private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow);
private static extern IntPtr GetForegroundWindow();

[DllImport(NativeDlls.User32)]
private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName);
private static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect);

private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport(NativeDlls.User32)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport(NativeDlls.User32)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport(NativeDlls.User32)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
private static extern bool IsIconic(IntPtr hWnd);

[DllImport(NativeDlls.User32)]
private static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport(NativeDlls.User32, EntryPoint = "SendMessage", SetLastError = true)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, uint wParam, IntPtr lParam);

[DllImport(NativeDlls.User32)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
private static extern bool SetForegroundWindow(IntPtr hWnd);

[DllImport(NativeDlls.User32)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

[DllImport(NativeDlls.User32)]
private static extern bool ShowWindow(IntPtr hWnd, uint nCmdShow);

#endregion

Expand Down Expand Up @@ -118,6 +134,57 @@ public void MinimizeWindow(string processName)
}
}

/// <summary>
/// Bring a window to the foreground reliably. Windows blocks
/// SetForegroundWindow from background processes by design (taskbar
/// flash only). The standard workaround is to attach the calling
/// thread's input queue to the current foreground window's thread,
/// during which SetForegroundWindow is permitted.
/// </summary>
private static void ForceForeground(IntPtr hWnd)
{
if (hWnd == IntPtr.Zero) return;

if (IsIconic(hWnd))
{
ShowWindow(hWnd, SW_RESTORE);
}

IntPtr foregroundHwnd = GetForegroundWindow();
uint foregroundThreadId = foregroundHwnd != IntPtr.Zero
? GetWindowThreadProcessId(foregroundHwnd, out _)
: 0;
uint targetThreadId = GetWindowThreadProcessId(hWnd, out _);
uint currentThreadId = GetCurrentThreadId();

bool attachedFg = false;
bool attachedTarget = false;
try
{
if (foregroundThreadId != 0 && foregroundThreadId != currentThreadId)
{
attachedFg = AttachThreadInput(currentThreadId, foregroundThreadId, true);
}
if (targetThreadId != 0 && targetThreadId != currentThreadId && targetThreadId != foregroundThreadId)
{
attachedTarget = AttachThreadInput(currentThreadId, targetThreadId, true);
}
BringWindowToTop(hWnd);
SetForegroundWindow(hWnd);
}
finally
{
if (attachedFg)
{
AttachThreadInput(currentThreadId, foregroundThreadId, false);
}
if (attachedTarget)
{
AttachThreadInput(currentThreadId, targetThreadId, false);
}
}
}

/// <inheritdoc/>
public void RaiseWindow(string processName, string executablePath)
{
Expand All @@ -126,7 +193,7 @@ public void RaiseWindow(string processName, string executablePath)
{
if (p.MainWindowHandle != IntPtr.Zero)
{
SetForegroundWindow(p.MainWindowHandle);
ForceForeground(p.MainWindowHandle);
Interaction.AppActivate(p.Id);
return;
}
Expand All @@ -143,7 +210,7 @@ public void RaiseWindow(string processName, string executablePath)
(nint hWnd1, int pid) = FindWindowByTitle(processName);
if (hWnd1 != nint.Zero)
{
SetForegroundWindow(hWnd1);
ForceForeground(hWnd1);
Interaction.AppActivate(pid);
}
}
Expand Down Expand Up @@ -228,7 +295,6 @@ public void TileWindows(string processName1, string processName2)
uint showWindow = 0x40;

// Restore windows first (SetWindowPos won't work on maximized windows)
uint SW_RESTORE = 9;
ShowWindow(hWnd1, SW_RESTORE);
ShowWindow(hWnd2, SW_RESTORE);

Expand Down
16 changes: 15 additions & 1 deletion ts/packages/agentRpc/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,21 @@ export function createRpc<
if (f === undefined) {
debugError("No call handler", message);
} else {
f(...message.args);
// Call handlers are fire-and-forget (no callId), so any
// synchronous throw cannot be reported back to the caller.
// Swallow it here to keep the RPC bus alive — otherwise a
// single bad notify (e.g. cancelCommand after the remote
// dispatcher channel disconnected) would surface as an
// uncaught exception in the host process.
try {
f(...message.args);
} catch (e: any) {
debugError(
"Call handler threw",
message.name,
e?.message ?? e,
);
}
}
return;
}
Expand Down
107 changes: 89 additions & 18 deletions ts/packages/agentServer/server/src/conversationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,57 @@ export async function createConversationManager(
idleTimeoutMs: number = DEFAULT_IDLE_TIMEOUT_MS,
): Promise<ConversationManager> {
const conversationsDir = path.join(baseDir, CONVERSATIONS_DIR);
await fs.promises.mkdir(conversationsDir, { recursive: true });

// Migrate old on-disk layout: "server-sessions/" → "conversations/"
// TODO: deprecate and remove this on-disk migration once enough time has
// passed that no production install still has a "server-sessions/"
// directory hanging around.
// Migrate old on-disk layout: "server-sessions/" → "conversations/".
Comment thread
TalZaccai marked this conversation as resolved.
// IMPORTANT: do this BEFORE creating the destination — otherwise
// `fs.rename` fails with EPERM/EEXIST on Windows when the target
// already exists, silently stranding all historical conversations
// in the old directory.
const oldConversationsDir = path.join(baseDir, "server-sessions");
try {
await fs.promises.rename(oldConversationsDir, conversationsDir);
debugConversation(
`Migrated on-disk directory "server-sessions" → "conversations"`,
);
} catch (e: any) {
if (
e?.code !== "ENOENT" &&
e?.code !== "EEXIST" &&
e?.code !== "EPERM"
) {
if (
!fs.existsSync(conversationsDir) &&
fs.existsSync(oldConversationsDir)
) {
try {
await fs.promises.rename(oldConversationsDir, conversationsDir);
debugConversation(
`Migrated on-disk directory "server-sessions" → "conversations"`,
);
} catch (e: any) {
debugConversationErr("Failed to migrate server-sessions dir:", e);
}
} else if (fs.existsSync(oldConversationsDir)) {
// Both directories exist — earlier builds raced and pre-created
// the destination. Move stragglers across so users don't lose history.
try {
for (const entry of await fs.promises.readdir(oldConversationsDir, {
withFileTypes: true,
})) {
const src = path.join(oldConversationsDir, entry.name);
const dst = path.join(conversationsDir, entry.name);
if (fs.existsSync(dst)) continue;
try {
await fs.promises.rename(src, dst);
} catch (e: any) {
debugConversationErr(`Failed to migrate ${entry.name}:`, e);
}
}
// Best-effort cleanup; will fail silently if non-empty.
await fs.promises.rmdir(oldConversationsDir).catch(() => undefined);
debugConversation(
`Merged stragglers from "server-sessions" → "conversations"`,
);
} catch (e: any) {
debugConversationErr(
"Failed to merge server-sessions stragglers:",
e,
);
}
}
await fs.promises.mkdir(conversationsDir, { recursive: true });
// Migrate old metadata filename: "sessions.json" → "conversations.json"
const oldMetadataPath = path.join(conversationsDir, "sessions.json");
const newMetadataPath = path.join(conversationsDir, METADATA_FILE);
Expand All @@ -127,6 +160,11 @@ export async function createConversationManager(

const conversations = new Map<string, ConversationRecord>();

// Single-flight lock for "auto-create the default conversation". Two
// concurrent first-connects could both observe "no conversations exist"
// and race; this serializes them so only one create happens.
let defaultCreateP: Promise<string> | undefined;

// Load persisted metadata
await loadMetadata();

Expand Down Expand Up @@ -315,6 +353,23 @@ export async function createConversationManager(
}
}

/**
* Throw if `name` collides (case-insensitive) with another existing
* conversation. `selfId` is excluded from the check so renaming a
* conversation to its current name is a no-op rather than an error.
*/
function ensureNameAvailable(name: string, selfId?: string): void {
const norm = name.trim().toLowerCase();
for (const [id, record] of conversations) {
if (id === selfId) continue;
if (record.name.trim().toLowerCase() === norm) {
throw new Error(
`A conversation named "${record.name}" already exists. Pick a different name.`,
);
}
}
}

// Sweep orphaned ephemeral conversations left behind by unclean CLI exits
{
const toSweep: string[] = [];
Expand Down Expand Up @@ -350,6 +405,7 @@ export async function createConversationManager(
const manager: ConversationManager = {
async createConversation(name: string): Promise<ConversationInfo> {
validateConversationName(name);
ensureNameAvailable(name);
const conversationId = randomUUID();
const createdAt = new Date().toISOString();
const record: ConversationRecord = {
Expand Down Expand Up @@ -391,9 +447,23 @@ export async function createConversationManager(
if (resolved !== undefined) {
return resolved;
}
// No conversations exist — auto-create a default
const info = await manager.createConversation("default");
return info.conversationId;
// No conversations exist — auto-create a default. Serialize so two
// concurrent first-connects don't both try to create "default" and
// race the duplicate-name check.
if (defaultCreateP === undefined) {
defaultCreateP = (async () => {
// Re-check inside the critical section in case another caller
// raced us between the early check above and acquiring the lock.
const existing =
getDefaultConversationId() ?? getAnyConversationId();
if (existing !== undefined) return existing;
const info = await manager.createConversation("default");
return info.conversationId;
})().finally(() => {
defaultCreateP = undefined;
});
}
return defaultCreateP;
},

async prewarmMostRecentConversation(): Promise<void> {
Expand Down Expand Up @@ -440,7 +510,7 @@ export async function createConversationManager(
// Notify existing clients that a new client has joined
if (sharedDispatcher.clientCount > 1 && dispatcher.connectionId) {
sharedDispatcher.broadcastSystemMessage(
`[A new client has joined this conversation. You are connected to '${record.name}'.]`,
`[A new client has joined this conversation.]`,
dispatcher.connectionId,
);
}
Expand Down Expand Up @@ -474,7 +544,7 @@ export async function createConversationManager(
// Notify remaining clients before this client leaves
if (record.sharedDispatcher.clientCount > 1) {
record.sharedDispatcher.broadcastSystemMessage(
`[A client has left this conversation. You remain connected to '${record.name}'.]`,
`[A client has left this conversation.]`,
connectionId,
);
}
Expand Down Expand Up @@ -518,6 +588,7 @@ export async function createConversationManager(
if (record === undefined) {
throw new Error(`Conversation not found: ${conversationId}`);
}
ensureNameAvailable(newName, conversationId);
record.name = newName;
await saveMetadata();
debugConversation(
Expand Down
11 changes: 10 additions & 1 deletion ts/packages/agentServer/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,13 @@ process.on("uncaughtException", (err) => {
// Log but do not exit for non-fatal errors.
});

await main();
await main().catch((err: any) => {
if (err?.code === "ERR_INSTANCE_LOCKED") {
// Friendly, single-line message — no stack trace for this expected
// case (another shell/server already owns the profile directory).
console.error(`\n[agent-server] ${err.message}\n`);
process.exit(1);
}
console.error("[agent-server] Fatal startup error:", err);
process.exit(1);
});
Loading
Loading