Skip to content

feat: /restart slash command + Claude adapter session reset#374

Merged
zomux merged 13 commits into
openagents-org:developfrom
baryhuang:feat/slash-command-restart
May 12, 2026
Merged

feat: /restart slash command + Claude adapter session reset#374
zomux merged 13 commits into
openagents-org:developfrom
baryhuang:feat/slash-command-restart

Conversation

@baryhuang
Copy link
Copy Markdown
Collaborator

Summary

Adds a typed /restart command in the OpenAgents Go (Swift) composer that clears the per-channel Claude CLI session so the next user message starts a fresh conversation. Used to recover from Anthropic's many-image-conversation rejection (>2000px-poisoned threads) without abandoning the channel — title, members, scrollback, and files all persist; only the agent's "remember the last N turns" is wiped.

⚠️ Stacked on PR #373. This branch is based on fix/go-composer-bubble-ime-paste. The diff currently shows PR #373's changes too. Once #373 merges to develop, that part disappears from the diff. Merge order: #373 first, then this.

Three pieces

Claude adapter (Python)

sdk/src/openagents/adapters/claude.py_on_control_action learns action="restart" alongside "stop". When invoked with payload.channel=<id>:

  • Kills the in-flight Claude subprocess for that channel only.
  • Drops the per-channel entry from _channel_sessions and persists. Next message arrives with no --resume → fresh Claude CLI session.
  • Posts a "Session restarted — next message starts fresh." status message.

Falls back to clearing all sessions if no channel param. Other adapters (Hermes, OpenClaw, Codex) ignore restart this round — follow-up PR.

Swift composer / chat view

  • Slash-command parser in ChatView.send() — trimmed text starting with / routes to handleSlashCommand instead of sendMessage. /restart fires store.restartSession; unknown commands surface via the existing lastError banner.
  • Autocomplete popup mirroring the React @mention pattern, lives only in the native composer. Floats above the input bar while the user types a one-line /-prefixed token. Tab inserts the highlighted command; Enter on an exact match falls through to send; Esc dismisses.
  • Keyboard routing via a new onSlashKey: ((Selector) -> Bool)? callback threaded through ComposerTextView and the NSTextView coordinator. Popup gets first crack at ↑/↓/Tab/Return/Esc before the composer's Return-to-send logic.

Architecture supports more commands by appending to availableCommands — only /restart ships today.

Swift store + model

  • Message.localStatus(channel:content:idPrefix:) generalizes the existing localStoppingStatus helper so the optimistic-status pattern serves both Stop and /restart.
  • WorkspaceStore.restartSession(sessionId:) fans out the restart control event to every agent in the session, mirroring stopAllAgents shape.
  • pollNewMessages drops local-restart- placeholders alongside local-stopping- when real agent status arrives.

Test plan

  • xcodebuild -scheme OpenAgentsGo_macOS Debug + Release and OpenAgentsGo_iOS Debug build clean.
  • Slash autocomplete: type / → popup appears with /restart. Type r → still matches. Type x → popup empty / hidden. Type space → popup hides.
  • Selection: ↓/↑ no-op with one item. Tab → input fills /restart. Esc → popup closes. Click row → input fills.
  • Send: with /restart in the input, press Enter → optimistic Restarting session… appears in the chat.
  • Daemon: Claude adapter logs Restart: cleared session for channel=…, kills any in-flight subprocess, and posts Session restarted — next message starts fresh. status.
  • Fresh-context proof: before /restart, agent recalls a fact from earlier in the thread. Run /restart. Ask again — agent reports no recall.
  • Unknown command: type /garbage and send → lastError banner shows Unknown command: /garbage. Available: /restart. No backend call.
  • Regression: typed Return-to-send still works for normal messages; image paste / Stop button / IME composition unaffected.

Deferred

  • Hermes / OpenClaw / Codex restart handlers — follow-up.
  • Context-menu / toolbar UI affordance — typed command only this round.
  • Confirmation alert — Enter-twice on the popup is the confirmation; menu paths would need it.

baryhuang added 3 commits May 8, 2026 21:04
Native Swift composer rebuild that addresses a stack of macOS/iOS issues
surfaced during dogfooding. Single round of work covering input, send,
and attachment paths.

Composer (ComposerTextView)
- Replace SwiftUI TextEditor with NSViewRepresentable / UIViewRepresentable
  wrapping NSTextView / UITextView so we can intercept Return-to-send
  around IME composition (hasMarkedText / markedTextRange) — fixes false
  sends mid-Pinyin / Kana composition.
- macOS: extend readablePasteboardTypes + acceptableDragTypes to include
  PNG / JPEG / TIFF / public.image / fileURL so Cmd+V validates and our
  paste(_:) override actually runs (was previously rejected at the menu
  validator → system bonk).
- macOS: paste(_:) handles PNG → JPEG → TIFF → NSImage fallback; file URL
  paste from Finder.
- iOS: subclass UITextView; canPerformAction / paste(_:) read
  UIPasteboard.images. Hardware Cmd+V works; soft Return inserts newline
  to match iMessage.

Bubble layout (Issue: empty padding on user bubbles)
- MessageBubble drops maxWidth: .infinity on the prose-only path so short
  messages hug their text; code/table content still claims full row.

Toolbar titles
- macOS: ChatView sets navigationTitle = current session title (replaces
  default "OpenAgents Go" in the window toolbar) with agent names as
  subtitle.
- ThreadListView gets a visible workspace name + slug header at the top
  on macOS (split-view sidebar doesn't surface .navigationTitle).

Stop button
- Add WorkspaceAPI.sendAgentControl mirroring the React app's pattern.
- WorkspaceStore.stopAllAgents fans out workspace.agent.control events
  with optimistic "Stopping..." status; retries once after 3s; clears the
  flag on terminal status from the message poll.
- ChatGPT-style: send arrow swaps to a red stop circle while an agent is
  working; grays out while a stop is in flight.

Attachment UX
- Dedup optimistic vs real attachment messages: rewrite optimistic
  content to finalContent in-place after api.sendMessage returns so the
  poll's content-equality dedup matches. Fallback matcher compares the
  set of attached filenames if exact-equality misses (mid-upload race).
  Fixes the "two bubbles per upload" symptom.
- ImageDownsampler (ImageIO-based): any pasted/picked/imported image
  whose longest side is > 2000px gets resized to 2000 and re-encoded as
  PNG. Targets the exact Anthropic many-image-request limit so a Retina
  screenshot can't poison the conversation.
- AttachmentChip: image variant shows a 48x48 thumbnail with X overlay
  (decoded via ImageIO, cached on the chip). Non-image keeps the
  Slack-style filename + size row.
- Long text paste >= 5000 chars converts to a Pasted-text-<ts>.txt
  attachment chip instead of dumping into the input field
  (matches ChatGPT's published threshold).

Bumps MARKETING_VERSION 0.2.1 -> 0.2.2.
Adds a typed /restart command in the OpenAgents Go (Swift) composer that
clears the per-channel Claude CLI session, so the next user message
starts a fresh conversation. Used to recover from Anthropic's
many-image-conversation rejection (>2000px-poisoned threads) without
abandoning the channel.

Claude adapter (sdk/src/openagents/adapters/claude.py)
- _on_control_action now recognizes action="restart" alongside "stop".
  When invoked with payload.channel=<id>:
  * Kills the in-flight Claude subprocess for that channel only (no
    spurious "Execution stopped by user" status — we'll post a clearer
    one below).
  * Drops the per-channel entry from _channel_sessions and persists.
    Next message → no --resume → fresh Claude CLI session, empty context.
  * Posts a "Session restarted — next message starts fresh." status
    message so the user sees what happened.
- Falls back to clearing all sessions if no channel param is provided.
- Other adapters (Hermes, OpenClaw, Codex) ignore the action this round.

Swift composer (packages/go/OpenAgents/Views/ComposerTextView.swift)
- New onSlashKey: ((Selector) -> Bool)? callback threaded through the
  NSTextView coordinator. textView(_:doCommandBy:) consults it before
  the existing Return-to-send logic, so the slash-command popup gets
  first crack at ↑/↓/Tab/Return/Esc when it's open.
- iOS path takes the parameter for source-call uniformity but ignores it
  (popup keyboard nav is macOS-only this round).

Swift chat view (packages/go/OpenAgents/Views/ChatView.swift)
- Slash-command interception in send(): trimmed text starting with "/"
  routes to handleSlashCommand instead of sendMessage. /restart fires
  store.restartSession; unknown commands surface via the existing
  lastError banner.
- Slash-command autocomplete popup mirrors the React @mention pattern.
  The popup floats above the input bar while the user types a one-line
  /-prefixed token, listing matching commands with description and icon.
  Tab inserts the highlighted command; Enter on an exact match falls
  through to the composer's send path; Esc dismisses.
- Architecture supports more commands by appending to availableCommands.

Swift store + model
- Message.localStatus(channel:content:idPrefix:) generalizes
  localStoppingStatus so /restart can reuse the optimistic-status
  placeholder pattern.
- WorkspaceStore.restartSession(sessionId:) fans out the restart
  control event to every agent in the session, mirroring stopAllAgents.
- pollNewMessages drops local-restart- (and any local-status-)
  placeholders alongside local-stopping- when real agent status arrives.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

@baryhuang is attempting to deploy a commit to the Raphael's projects Team on Vercel.

A member of the Team first needs to authorize it.

baryhuang added 10 commits May 8, 2026 23:46
Adds /status as a second slash command alongside /restart. Users can
type /status (or pick it from the autocomplete popup) to see the remote
daemon's uptime, openagents version, agent type, and network.

Daemon (Python — base adapter)
- BaseAdapter.__init__ tracks self._started_at = time.time().
- BaseAdapter._on_control_action now handles action="status" — posts
  back a "**Agent status**" chat message with name, type, openagents
  package version (via importlib.metadata), uptime (humanized:
  "3h 12m" / "45m 7s" / etc.), and network.
- Putting it in the base means all adapter types report status
  uniformly without per-adapter implementations.

Daemon (Python — Claude adapter)
- _on_control_action now calls await super()._on_control_action(...)
  for actions it doesn't recognize, so the base-class status handler
  runs after stop/restart fall-through.

Swift app
- /status entry in ChatView.availableCommands (autocomplete popup).
- /status case in handleSlashCommand → store.requestSessionStatus.
- WorkspaceStore.requestSessionStatus(sessionId:) fans out the status
  control event to every agent in the session (mirroring restartSession),
  with an optimistic "Checking status…" placeholder.
- Unknown-command error message updated: "Available: /restart, /status".

Architecture supports more commands by appending to availableCommands
and adding a case in handleSlashCommand. /status validates the
fall-through-to-super pattern in adapter overrides.
As the user types after the leading slash, each suggestion's name renders
the matched prefix in bold and the unmatched tail in muted gray, so it's
visually clear what's narrowing the list. The leading slash itself is also
muted so the bolded match stands out.

Standard pattern from Slack / VSCode command palette / Discord — kicks in
once we have more than one slash command (now /restart and /status).
Mirrors the existing 'stopped'/'stopping failed' handling for stop. Without
this, the typing/busy indicator stayed pinned forever after /restart
because the 'Session restarted - next message starts fresh' status message
didn't match the terminal regex, so isAgentWorking still returned true.
After typing /restart, the user sees:
1. 'Restarting session…' (optimistic)
2. 'Session restarted - next message starts fresh.' (terminal status, busy
   indicator clears thanks to the regex update in the previous commit)
3. 'Checking status…' (optimistic, from the chained /status)
4. '**Agent status**' block with Uptime: 0s, version, network — proves the
   agent actually restarted

Control events are FIFO per agent, so order is preserved at the daemon.
When /restart is invoked, the per-channel Claude session-id is cleared but
the agent process keeps running. Without this change the auto-followup
/status reported the old process uptime, making it look like the restart
didn't take effect — a user reported seeing 'Uptime: 11m 52s' immediately
after a successful /restart.

Resetting _started_at on restart shifts the semantic from 'process uptime'
to 'uptime since last restart', which is what 'restart' suggests anyway
and matches the standard server-uptime convention.
Previously /restart did an in-process session-clear: kill the in-flight
Claude subprocess, drop the channel's session UUID, reset _started_at.
The agent process kept running, so 'uptime' lied about what 'restart'
had done.

Now /restart writes `restart:<agentName>` to ~/.openagents/daemon.cmd,
the same IPC the `agn restart <name>` CLI uses. The daemon's
command-file poller picks it up within ~1s, calls restartAgent, the
current adapter's run() loop exits cleanly via the checkStop interval,
and the daemon spawns a fresh adapter instance — new _started_at, new
client connection, sessions reloaded from the on-disk file (which we
already cleared the channel from).

Sibling agents on the same daemon (opeagents-bot, company-os-bary, etc.)
are untouched. The Node daemon process itself stays up.

The pre-bounce work — kill subprocess, persist cleared session, post the
'Session restarted' status — happens BEFORE the IPC write so it lands
while we're still online. If the IPC write fails (daemon crashed, fs
error), we fall back to resetting _started_at in-process so the next
/status still shows uptime reset.
The deployed openagents-org/agent-launcher runs the JavaScript adapters
under sdk/.openagents/nodejs/.../src/adapters/. The Python files in
sdk/src/openagents/adapters/ are not used by the daemon and were only
being maintained for parity. Per user direction, drop the parity work
and let the JS adapter be the source of truth.

Restores base.py and claude.py to their develop state. The /restart and
/status features remain fully implemented in the JS adapter (deployed
out-of-tree) and in the Swift app (in-tree).
…side

These are the daemon-side changes that pair with the Swift slash-command
work in packages/go. Tested against the deployed copy at
`agents-api.caremojo.app` (network 0048fff6, agent lovecareliving).

base.js:
- _startedAt timestamp set in constructor; used by status reporter.
- _onControlAction handles 'status' → posts an 'Agent status' chat
  message with name, type, agent-launcher version, uptime, network.
- _skipExistingControlEvents advances _lastControlId past existing
  events on startup so a fresh adapter (after a restart bounce) doesn't
  re-process the same restart event and trigger a loop.
- run() reorder: _skipExistingControlEvents + heartbeat + control poll
  start IMMEDIATELY after join, before the message-cursor advance.
  Slash commands are no longer gated on _skipExistingEvents.

claude.js:
- _onControlAction handles 'restart': kills the in-flight Claude
  subprocess for the channel, clears the channel's session UUID,
  posts the 'Session restarted' status, then writes 'restart:<name>'
  to ~/.openagents/daemon.cmd. Daemon's command-file poller picks it
  up and bounces just THIS agent — sibling agents on the same daemon
  are untouched.
- Falls through to super._onControlAction(...) for unknown actions
  so the base 'status' handler still runs.

workspace-client.js:
- pollControl now requests sort=desc + limit=10. With the default ASC
  sort, a busy workspace's limit window filled with ancient stop
  events for unrelated agents and nothing matched. After fetching, we
  re-sort filtered results ascending so callers process oldest-first.

daemon.js:
- restartAgent now waits up to 20s for _adapters[name] to actually
  disappear before calling _launchAgent. stopAgent's existing 5s wait
  was too short for graceful shutdown (control-poller cleanup,
  disconnect, in-flight kill), so the launch hit the duplicate-launch
  guard and the agent stayed stuck in 'stopped' with no relaunch.

docs/:
- slash-test-{1,2,3}.png: screenshots from the Swift app showing
  /status autocomplete, /restart autocomplete, and the /restart →
  bounce → /status confirmation chain (uptime resets to 4s).
When the user taps the input on iPhone, the keyboard popped up and
immediately dismissed before they could type a character. Cause: the
iOS updateUIView path was calling resignFirstResponder() whenever the
FocusState binding read 'false', and SwiftUI re-renders during normal
state propagation can briefly produce that stale 'false' read right
after textViewDidBeginEditing fires.

Drop the auto-resign branch — UIKit already dismisses the keyboard
naturally (tap outside, Done key, etc.). The auto-PROMOTE on
wantsFocus=true stays. Now matches the macOS path which never had
this branch.
@peakmojo
Copy link
Copy Markdown

peakmojo commented May 9, 2026

Screenshot 2026-05-09 at 1 36 46 AM Screenshot 2026-05-09 at 1 36 41 AM Screenshot 2026-05-09 at 1 36 33 AM

Ready for review.

Copy link
Copy Markdown
Contributor

@zomux zomux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session reset logic is correct — sessions are cleared and persisted before daemon bounce, and _skipExistingControlEvents prevents restart loops. Stacked on #373 which is now merged. Minor suggestions (non-blocking):

  1. Empty catch {} blocks in _skipExistingControlEvents and claude.js should log warnings — silent failures in the control-event cursor path could mask issues
  2. Consider a client-side cooldown on /restart to prevent rapid daemon bounces
  3. daemon.cmd write uses string interpolation with agentName — low risk since it's from config, but sanitizing at registration time would be defensive

@zomux zomux merged commit 2205071 into openagents-org:develop May 12, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants