feat(triggers): a meet trigger whose semantics are a meeting - #191
Merged
Conversation
An agent joins a meeting over tty today. tty is documented as "each connection is a conversation", which is right for a terminal and the opposite of what a meeting needs: the meeting is the conversation, and connections come and go inside it while a laptop sleeps or a bridge reconnects. The transport is not the problem, so meet keeps it. WebSocket is right for this and the frame vocabulary is already JSON. What changes is what a connection means. The meeting id arrives in a required join frame rather than an optional query parameter, and the key is meet:<meeting_id>. A connection that never names a meeting is refused rather than handed an invented id whose history nothing could reach again, which is the tty failure #189 is about; meet sidesteps it by construction rather than warning about it. Two frames a terminal has no concept of: participant, which is recorded without spending a turn because a busy room would otherwise start a run per arrival, and end. end is the one that earns the trigger. A closed socket is ambiguous between a dropped connection and a finished meeting, and the agent cannot tell which. end says so while the conversation is still in context, and with summarize_on_end set the agent spends one more turn writing the record. That turn lands in the transcript, so the record outlives the meeting. Without the setting an ending costs no model call, because a summary per meeting is not something every agent wants. tty is untouched and stays a terminal. The shared transport pieces (image validation, the constant-time token compare) moved to ws_session.ts with no behaviour change. Refs #190. The remaining half is report_to: a scheduled result that fires after the meeting still falls back to a log line, because delivery to another trigger is a service-level concern rather than a trigger one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPftW3F8JTuivRhFmoY1wC
Caught by the bridge side while designing against this: a bridge can race itself into sending end twice, most likely a reconnect delivering an end that was already in flight. The trigger recorded the meeting as ended and then ran the summarize turn regardless, so one meeting could pay for two model calls. That is the kind of duplicate that only shows up on the bill. A second end on a meeting already ended closes the socket and returns. Also pins the other half of that conversation: the bridge fires end and goes rather than waiting for the summary, so the summary frame lands on a closed socket. That is fine and now has a test saying so — send() already no-ops on a closed socket, and the run still completes and still writes to the transcript. The transcript is the record; the frame is a convenience for a client that happens to linger. It does make report_to the real delivery answer rather than a nicety, which is worth knowing before this ships. Refs #190 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPftW3F8JTuivRhFmoY1wC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the trigger half of #190. Now based on
main, since the previous six merged.Why not just use
ttyAn agent joins a meeting over
ttytoday, anddocs/tty.mdxstates its contract:That is right for a terminal, where a new window should be a fresh start, and backwards for a meeting. The meeting is the conversation; connections come and go inside it while a laptop sleeps, a bridge reconnects, or a second client attaches.
The transport is not the problem, so this keeps it. WebSocket is right here: bidirectional, long-lived, and the frames are already JSON. Streaming HTTP would be worse, since SSE is one-directional and input would need a second connection with its own lifetime. Media stays the bridge's job. What changes is what a connection means.
What is different from tty
joinis required and carries the meeting id. The key ismeet:<meeting_id>. A connection that never names a meeting is refused rather than handed an invented one, so #189's failure mode cannot occur here by construction rather than by warning.participantrecords who came and went without spending a turn. A busy room would otherwise start a run per arrival.endis distinct from the socket closing, and it is the frame that earns the trigger. Today a closed socket is ambiguous between a dropped connection and a finished meeting, and the agent cannot tell which, so it can never reliably write the record.endsays which happened while the conversation is still in context.summarize_on_endis opt-in:Set it and
endspends one more turn; the reply comes back as{type: "summary", text}and lands in the transcript, which is the record that outlives the meeting and the thing #173 needs. Leave it out and an ending costs no model call, because a summary per meeting is not something every agent wants.What stays the same
ttyis untouched and remains a terminal. The genuinely shared pieces (image validation, the constant-time token compare) moved tows_session.tswith no behaviour change; the tty tests still pass unmodified.Testing
515 pass,
deno task okclean. Eight new, each pinning a way this differs from a terminal:join, and an emptymeeting_idis refusedendwrites the record, and the summary is in the transcript rather than only on the wiresummarize_on_end, an ending adds no messages and sends no summaryparticipantframes add no turns, and a malformed one errorstriggersFromConfigbuilds it and names a missing token env varWhat is not done
report_to. An agent can schedule follow-up work during a meeting; the schedule survives restarts and the work runs. When it fires after the meeting there is no open socket,deliverreturns false, andAgentService.#deliverfalls back to alogInfotruncated at 200 characters. Routing that to another trigger is a service-level concern rather than a trigger one, so it is still open on #190.A meeting that ends without an
endframe. If a bridge cannot always tell that a meeting finished, the record never gets written. Documented rather than papered over, and it is the first question I asked the bridge side to answer.The bridge side is being written in parallel against this frame vocabulary.