Skip to content

Phase 2: trust that grows (plans, learned rules, undo window) - #7

Merged
Taskmaster-1 merged 10 commits into
mainfrom
feat/phase2
Oct 25, 2026
Merged

Taskmaster-1 merged 10 commits into
mainfrom
feat/phase2

Conversation

@Taskmaster-1

Copy link
Copy Markdown
Owner

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

  • The new built-in propose_plan tool lets the agent list the exact calls it intends to make. You see a checklist, untick what you don't want, and approve once.
  • Each kept step becomes a single-use grant tied to the exact tool and input. When the agent makes that call it runs without asking, even in a tainted run, because you saw that exact payload. A different input still asks, and a block rule still wins.
  • Grants only come from a human sign-off: a plan auto-approved by a rule grants nothing.
  • Batch decisions: several pending calls from one step show as one "This task wants to do 3 things" card. New endpoint: POST /api/runs/:id/tool-calls/decisions. It is all or nothing and uses compare-and-set.

Approvals that learn

  • After 5 or more unedited approvals of a tool, with no denial or edit in the last 10, Synapse suggests an ordinary allow rule. You can see it, edit it and remove it.
  • Recipients generalize through a new emailsEndWith condition: 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.
  • Send and destructive tools are never suggested tool-wide.
  • Tainted runs still ask, whatever the rules say.
  • It shows in the Inbox and after an approval, with Allow or Not now.

Undo window

  • Person-approved send and destructive actions wait undo_seconds (default 10; Off, 5, 10 or 30 in Settings) with a live "Sending in 9s · Undo".
  • The run is parked with its lease at the deadline, so nothing spins.
  • Undo is a compare-and-set race with the worker's approved → executing transition, so exactly one wins and nothing runs twice.
  • Auto-allowed calls and plain writes are not delayed.

Fixed along the way

  • Live updates were held back by the web proxy. Next's rewrite proxy gzips responses and buffered small SSE writes, so status changes after an approval didn't reach the page until something else was sent. Run event streams now send Cache-Control: no-cache, no-transform, and a regression assertion covers it. This affected live updates app-wide, not just Phase 2.
  • Field name: batch approval takes edited input as input, like the single route.
  • Index: added on tool_calls (user_id, decided_at) for the suggestions query.

Schema

One migration, 0004_trust_that_grows:

  • runs.plan_grants
  • tool_calls.execute_after
  • user_settings.dismissed_suggestions
  • user_settings.undo_seconds
  • the new index

Breaking changes

  • POST .../tool-calls/:id/decision now returns 200 { executeAfter } instead of 204.

Testing

  • 143 tests pass, plus lint, typecheck and the web build. New tests cover:
    • plan grants (exact match, single use, block beats grant, tainted runs);
    • batch decisions;
    • the suggestion engine (thresholds, domains, Bcc, dismissals, ids);
    • the undo window (timing, a single run, undo, races, cancel).
  • Clicked through in the browser against a scripted-model server with fake Gmail, in light and dark, on desktop and mobile. Checked: approving a plan with a step unticked, Approve all, the live countdowns, Undo on one email, and the suggestion appearing after six approvals. No console errors and no overflow.

🤖 Generated with Claude Code

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
@Taskmaster-1
Taskmaster-1 merged commit 8598641 into main Oct 25, 2026
1 check passed
@Taskmaster-1
Taskmaster-1 deleted the feat/phase2 branch October 25, 2026 01:22
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