Skip to content

feat: notify participants when their agent run finishes or fails - #255

Merged
iamnbutler merged 4 commits into
mainfrom
agent-done-notify
Oct 14, 2026
Merged

iamnbutler merged 4 commits into
mainfrom
agent-done-notify

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Part of #12. Covers finished and failed runs. Waiting-for-answer attention is left out because it depends on #21, and Ace has no question tool yet.

Channel. #runs now keeps each live run's inputs from pi's LiveDoc. When LiveDoc.run goes away, the same commit carries pi's settlement of those inputs:

  • done maps to done
  • unanswered/aborted maps to stopped
  • anything else (model_error, no_model, faulted, …) maps to failed

The author is the newest input entry the run answered that has a human author, found with one tx.entry read per input. Transcripts are not scanned. Runs whose inputs all have agent@… authors publish nothing; these are subagent chats and cross-channel messages. The new option onRun(run: RunEnd) publishes { chat, at, outcome, author }. RunEnd lives in @ace/channel/protocol.

Host. The worker writes the latest end into the catalog record as ran. Hosted channels send { ran } over WorkspaceMessage. Listings expose ran?: RunEnd. Like active, it is a hint that persists across worker restarts. pi still owns the runs, and losing ran loses only pending attention.

App (apps/app/src/attention.ts):

  • Storage: the app keeps { since, seen, told } in browser storage, keyed by host URL and user. since is the first visit, so history never lights up. told dedupes signals across reloads and reconnects.
  • Rows: an unseen end of a run the current user started marks that channel's row. done gives unreadCount: 1 (a dot, "1 new message"). failed gives needsAttention (the wave, "Agent needs your attention"). stopped is ignored.
  • Seen: an end is seen once its channel is open in a visible, focused window. Regaining focus while it is open also marks it seen.
  • Signals: each new end produces one signal. If the window is focused, the existing Layout glow pulses: 1.8 s, opacity 0.6, fade in 0.8 s, fade out 1.3 s. If the window is unfocused and the app runs in the desktop, a new authenticated notify DesktopRPC request calls Utils.showNotification with the channel name as title and "Agent finished" or "Agent run failed" as body.
  • Settings: none added.

Docs. docs/architecture.md covers the projection and explains that this is per-user client attention, not lobbies.

Verification

All of this ran against a real source host with an isolated ACE_HOME, ACE_CONFIG_HOME, ACE_PORT=4791, a non-existent ACE_KEYCHAIN_SERVICE (so there were no keychain prompts) and ACE_USER. The only model key available without prompting was OPENAI_API_KEY, and OpenAI rejects it with 401.

  • Existing channel: I created a channel with origin/main code and ran it once (that run failed), then reopened it on this branch. It opened unchanged, and its listing had no ran until its next run.
  • failed: a run in that channel produced ran: { outcome: "failed", author: "tester" } in the catalog and in gateway listings.
  • stopped: an ask followed by a stop 0, 50 and 150 ms later produced outcome: "stopped" each time.
  • Worker restart: after SIGTERM the dormant listing still carried ran. A run interrupted by SIGTERM resumed when the channel reopened, then published its end. run.publish logged no warnings.
  • App: I built the app, had the host serve it, and drove it with headless Chromium through Playwright:
    • With channel A open, a failed run in B (same user) pulsed the glow (data-loading appeared within 100 ms and cleared after the pulse).
    • B's row showed "Agent needs your attention".
    • After a reload B was still marked and did not pulse again.
    • Opening B cleared the mark and updated seen.
    • A stopped run in A while B was open added no mark.
  • bun types and bun run ci pass (265 files).

Not verified:

  • done: no working model key was available without the keychain, so this outcome is untested and so is the dot it produces. It uses the same path, with pi's done settlement.
  • Subagent/agent-authored runs: not exercised, because this also needs a working model.
  • Native notification: only type-checked. Headless Chromium has no desktop bridge.
  • Hosted channels: the { ran } path is only type-checked. No hosting service was deployed.

Limitations

  • Electrobun 1.18.1's showNotification has no click callback, so the native notification can't link to the chat.
  • The row selects the channel, not the specific chat.
  • A done row's expanded text reads "1 new message".

Built in Ace

No. Made by a Claude Code subagent in a terminal at the maintainer's choice for this session; not an Ace capability gap. Dogfooding: #5.

Channels publish each run a person invoked as it ends (done, failed, or
stopped, with the author of the newest human input it answered). Hosts
project the latest end into the catalog and listings as `ran`, carried
over the workspace socket for hosted channels. The app marks the row of
an unseen end the current user started (dot for done, attention mark for
failed), pulses the window glow when another channel is open, and sends a
native desktop notification when the window is unfocused.

Part of #12. Waiting-for-answer attention depends on #21.

Why: packages/channel/src/channel.ts#fn #ended | pi settles a run's inputs in the same commit that removes LiveDoc.run; read the outcome from that commit's submission changes, not from the transcript
Why: packages/channel/src/room.ts#fn isAgent | subagent and cross-channel inputs are authored agent@...; runs they start must not notify a person
Why: apps/app/src/attention.ts | `told` persists per host and user so reloads and reconnects never re-notify the same run end; `since` keeps history from lighting up on a first visit
@iamnbutler
iamnbutler merged commit a0005df into main Oct 14, 2026
1 check 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.

1 participant