Repository navigation
Phase 2: trust that grows (plans, learned rules, undo window) - #7
Merged
Merged
Conversation
After 5+ unedited approvals of a tool (and no denial or edit in its last 10 decisions), Synapse offers an ordinary allow rule. Recipients sharing one non-public email domain become an allEndWith condition; a single repo, project, channel or team becomes equals. Send and destructive tools are never suggested tool-wide. Tainted runs still ask, as before. - core: pure suggestRules() with stable ids, plus unit tests - server: GET /api/policy/suggestions, POST :id/accept and :id/dismiss - db: user_settings.dismissed_suggestions text[] - web: suggestion strip in the inbox, a prompt under a fresh approval, and a Learned pill on rules in settings
- Batch decisions: POST /api/runs/:id/tool-calls/decisions decides several
awaiting calls of one run atomically (decideToolCalls; decideToolCall now
delegates to it). Re-queues the run once when nothing is left awaiting.
- propose_plan built-in tool: always asks; approving with steps unticked sends
an edited input with only the kept steps. When it runs after a person's
sign-off, each kept step becomes a grant (tool + SHA-256 of canonical input)
in the new runs.plan_grants column.
- Runtime: a call exactly matching an unused grant is allowed ("Part of the
plan you approved"), even in a tainted run; block rules still win; each
grant is used once.
- Web: runs with 2+ open asks show one checklist card in the task view and
inbox; plan approvals render as a tickable list of steps.
- System prompt asks the agent to propose a plan before multi-step changes.
- Tests for grants (core) and batch + end-to-end plan flow (server).
# Conflicts: # apps/web/app/(app)/inbox/page.tsx # apps/web/components/task/task-view.tsx # packages/db/drizzle/meta/0004_snapshot.json # packages/db/drizzle/meta/_journal.json
- New emailsEndWith condition: every address anywhere in the call (to, cc, bcc, attendees, any field) must end with the domain, so an added bcc still asks - No suggestion for a tool the user deliberately set to ask - Index tool_calls (user_id, decided_at) for the suggestions query
When a person approves an action that reaches other people or can't be
taken back (effect send or destructive), it now waits out a short window
before it runs, like undo send in Gmail.
- user_settings.undo_seconds (default 10, 0 is off, max 60), exposed in the
settings API and in Settings > General (Off / 5s / 10s / 30s).
- tool_calls.execute_after. decideToolCall opens the window through the
undoDelay/startUndoWindow helpers (database clock) and re-queues the run
with lease_expires_at at the end of the window (runnableAt), so no worker
takes it early. Auto-allowed calls, reads and writes never wait.
- Runtime: reads in a step run at once; writes run in proposal order and
stop at the first one still inside its window. runAgent then returns
"running"; the worker releases the run for nextUndoWaitMs and wakes
workers when it ends. approved -> executing stays the only way to run.
- POST /api/runs/:id/tool-calls/:toolCallId/undo: compare-and-set
approved -> denied while execute_after > now(), 409 after. The model is
told "The user undid this action before it ran." and the run resumes now.
- The decision endpoint returns { executeAfter }; run detail, chat
snapshots and tool.status events carry executeAfter. Stopping a task
inside the window cancels it at once.
- Web: receipts, inbox rows and home rows show "Sending in 9s · Undo"
with a fuse that respects reduced motion, then "Undone" or the normal
running/done state. The run panel shows the countdown too.
# Conflicts: # apps/server/src/routes/runs.ts # apps/web/app/(app)/inbox/page.tsx # apps/web/components/task/task-view.tsx # apps/web/lib/api.ts # docs/api.md # packages/core/src/runtime.test.ts # packages/db/src/runs.ts # packages/db/src/schema.ts
- Run event streams send Cache-Control: no-transform. Next's rewrite proxy gzips responses and held back small writes, so status changes after an approval (and the undo countdown) didn't arrive until something else was sent - Batch approvals open undo windows per call, emit them live, and park the run - Plan cards explain what approving does; a reason shared by every batch row is said once
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.
Phase 2 of making SynapseAI stronger. Approvals stop being a chore while staying in your hands. The three features were built in parallel and integrated here.
Approve the plan, not every step
propose_plantool lets the agent list the exact calls it intends to make. You see a checklist, untick what you don't want, and approve once.POST /api/runs/:id/tool-calls/decisions. It is all or nothing and uses compare-and-set.Approvals that learn
emailsEndWithcondition: every email address anywhere in the call must match. The suggested rule says "every address ends with @acme.com", so an added Bcc to an outsider still asks. Public mail domains never generalize.Undo window
undo_seconds(default 10; Off, 5, 10 or 30 in Settings) with a live "Sending in 9s · Undo".Fixed along the way
Cache-Control: no-cache, no-transform, and a regression assertion covers it. This affected live updates app-wide, not just Phase 2.input, like the single route.tool_calls (user_id, decided_at)for the suggestions query.Schema
One migration,
0004_trust_that_grows:runs.plan_grantstool_calls.execute_afteruser_settings.dismissed_suggestionsuser_settings.undo_secondsBreaking changes
POST .../tool-calls/:id/decisionnow returns200 { executeAfter }instead of 204.Testing
🤖 Generated with Claude Code