Commit graph

2124 commits

Author SHA1 Message Date
gagan
f68f546223
Merge pull request #748 from rowboatlabs/feat/granola-parity
Granola-parity meeting capture: resident app, ambient detection, auto-stop + notes redirect
2026-07-14 14:34:42 +05:30
gagan
5f71ff2000
Merge pull request #746 from rowboatlabs/fix/workspace-watcher-scope
fix(workspace): scope the workspace watcher to whitelisted roots (EMFILE crash)
2026-07-14 13:48:31 +05:30
Gagan
827bd4df5e feat(x): popup countdown line, arrival chime, faster call-end
- 2px progress line drains toward auto-dismiss (now 30s); hovering the
  popup freezes both the line and the dismissal. The renderer owns the
  countdown; main keeps only a 3-min crash-safety fallback
- soft synthesized arrival chime (WebAudio — no asset, no spawn)
- close button in the top-left corner, hover-revealed
- call-end grace 3s → 1s with a 1s detector poll: notes flow starts
  within ~2s of hang-up
2026-07-14 13:42:03 +05:30
Gagan
786382c840 fix(x): meeting popup window is the card — kills the grey backing slab
macOS NSPanel windows don't honor transparent: true (nor a #00000000
backgroundColor); the transparent margins around the card rendered as an
opaque grey slab. Drop transparency entirely: size the window to the
card (376×48), paint the card color on the window, use native rounded
corners + shadow. The hover × slides in over the card's left edge
instead of overhanging a corner.
2026-07-14 13:33:29 +05:30
PRAKHAR PANDEY
1e8e36ebfb
Merge pull request #738 from rowboatlabs/feat/settings-models-picker
feat: connect-only model settings with dynamic composer model picker
2026-07-14 13:27:46 +05:30
Prakhar Pandey
7ddc82f25d feat: remove model-name entry from onboarding to match connect-only settings
Onboarding still asked for a model name for openrouter/aigateway/ollama/
openai-compatible. It now mirrors the settings dialog: entering a key (or
base URL) is enough and the model is resolved silently from the fetched
list (same precedence), with openai-compatible keeping a visible Model
field as its escape hatch. Save payload shape and multi-provider flow
unchanged.
2026-07-14 13:10:52 +05:30
Prakhar Pandey
e74ce4e9a5 fix: pin picker search input to top and connect providers in a single click
Bug 1: the composer picker search input lived inside the scrolling dropdown
body as a sticky element, so container padding and Radix's open-focus
scroll left it slightly below the top. Moved it out of the scroll area into
a fixed header with the model list in an inner scroller, so it's flush at
the top and always visible.

Bug 2: Connect gated on resolvedModel, which is empty while the debounced
key-change fetch is still in flight right after pasting a key, so the first
click was a no-op. Connect now gates on credentials only, and the save
handler resolves the model on demand (one fetch, same silent precedence)
when it hasn't loaded yet. Save payload, models:test gate, and per-function
fields unchanged.
2026-07-14 12:38:00 +05:30
arkml
7cfc2200ba
Merge pull request #750 from rowboatlabs/email_improvements
feat(x): unified email classifier with labels, triage sections, and user instructions

Replace the two disjoint email classification systems (inline importance
classifier + batch labeling agent) with a single LLM pass that emits
importance, category, knowledge verdict, summary, and a pre-drafted reply.

Classification:
- classify_thread.ts now returns {importance, category, knowledge}; the
  category enum and prompt are built at call time from a label registry
  (7 built-ins named after Superhuman's vocabulary — Direct, Calendar,
  News, Marketing, Pitch, Notification, Receipt — plus up to 12 custom)
- Users create labels in plain text via the new "Email agent instructions"
  dialog; an extraction pass syncs them into config/email_labels.json.
  Instructions are also injected into every classify/draft call and the
  composer's Write-with-AI bar (seeded: Investor / Candidate / Customer)
- Per-thread category corrections are sticky and feed few-shot learning
  (email_category_feedback.ts), same pattern as importance corrections

Knowledge graph:
- Delete the labeling agent pipeline (label_emails, labeling_agent,
  labeling_state); admission is now the stamped `knowledge: extract|skip`
  frontmatter, with legacy noise-tag fallback and a budgeted sweep that
  backfills unclassified markdown (build_graph holds unstamped files)
- Guardrails: user-participated threads always extract; user importance
  flips never touch the knowledge verdict; classify failures stamp nothing

Inbox UI:
- Sections become Needs you / Waiting on them / Everything else; waiting
  state is derived per render (user sent last, others participated), never
  stored, so it can't go stale
- Category chips on rows (Direct stays chipless), filter pills with counts,
  one-click bulk archive per category, "Reply ready" indicator
- Fix: Everything else section could never load/render under sustained
  sync writes (fetch was chained on hasReachedEnd, which live reloads reset)
2026-07-14 11:54:44 +05:30
Arjun
8436e0e245 allow labels and drafts to be steered by the user in the app 2026-07-14 11:40:17 +05:30
Arjun
b2158f320f add email improvements 2026-07-14 10:02:22 +05:30
Gagan
9ee332e21b style(x): tray recording waveform + popup polish
- animated sine-wave indicator (braille dot bars, two per cell, 300ms
  frames) beside the tray icon while a meeting records
- popup: hover-anywhere reveals the close button (the drag region was
  swallowing mouse events), leaner card (48px, 76px window), sits 44px
  below the menu bar
2026-07-14 01:27:32 +05:30
Gagan
f2d6bbbff3 style(x): refine meeting-detected popup
- leaner: 400×84 window, 56px card, type stepped down (14/13px)
- close button only appears on hover
- Rowboat sail sits directly on the button background (no chip box),
  slightly larger
- dropped the dashed/solid left rule; symmetric margins and padding
- browser preview mode (vite only, no Electron): renders sample data at
  the real window size on a backdrop — #meeting-detected, plus
  ?variant=calendar for the calendar-linked look
2026-07-14 01:06:46 +05:30
Gagan
3419cf8ef0 fix(x): don't overwrite same-titled meeting notes within a day
Note filenames derive from the meeting title, so every ad-hoc detected
meeting (titled "Meeting") — and any recurring calendar event repeating
its summary — resolved to the same file and each recording clobbered the
previous one's notes. Suffix with the start timestamp when the path
already exists.
2026-07-14 00:51:11 +05:30
Gagan
95f232cca9 feat(x): auto-stop on call end + redirect to finished notes (Granola parity phase 3)
- mic-monitor now resolves each mic owner's bundle ID (CoreAudio) and
  executable path (libproc) natively — the main process never shells out
  during capture (child-process spawns fail with EBADF while Electron
  holds a capture session; this bit the ps-based first cut)
- detector matches owners by bundle-ID prefix (com.google.chrome,
  us.zoom, com.tinyspeck.slackmacgap, …) with executable-name fallback,
  and recognizes Rowboat's own capture (com.rowboat / dev electron)
- call-end detection: once a meeting app has been seen on the mic during
  a recording, its absence for 3s means the call ended → auto-stop →
  summarize — same flow as a manual stop. Arms only when a meeting app
  was actually observed, so in-person recordings never auto-stop
- post-summary redirect: Rowboat foregrounds itself on the finished note
  (app:focusMainWindow now steals focus); the new meeting_notes_ready
  notification stays as a background-only fallback with a toggle in
  Settings > Notifications
2026-07-14 00:37:10 +05:30
Prakhar Pandey
1a8367ada4 fix: gate OpenRouter connection on the authenticated /models/user endpoint
OpenRouter's public /api/v1/models returned the full catalog even with an
invalid key, so Settings showed Connected while the model call failed with
Missing Authentication header. With a key present we now fetch the
account-scoped /models/user with the Bearer token, so a bad key surfaces
as an error instead of a false Connected; no key keeps the public preview.
Other providers untouched. Note: unlike the rest of this PR, this touches
core (models.ts).
2026-07-13 23:07:01 +05:30
Gagan
66865b4f6b feat(x): ambient meeting detection + take-notes popup (Granola parity phase 2)
- mic-monitor Swift helper: polls CoreAudio for mic-in-use, with owning
  PIDs on macOS 14.4+ (process-object API) so calls attribute to the app
  that actually holds the mic (Chrome vs Zoom vs Slack); no TCC permission
  needed. Compiled best-effort by bundle.mjs; exits when stdin closes
- core meetings/detector: 5s mic debounce, PID→process attribution with
  running-app fallback, Granola labels (Huddle/Call/Meeting detected),
  calendar merge (event start −15min through end), once per mic session
  (30s idle reset), suppressed while Rowboat itself captures (recording
  or assistant voice call), gated by a meeting_detection notification
  category (Settings > Notifications)
- popup: lean Granola-style bar (title + platform + Take notes pill,
  floating ×) in a transparent non-activating panel at screen-saver level
  with fullscreen-auxiliary behavior — floats over fullscreen meetings on
  the cursor's display; activation policy restored immediately so the
  Dock icon survives and Take Notes can foreground Rowboat
- Take Notes routes into the existing take-meeting-notes flow with a
  synthetic or matched calendar event (source: detected); never records
  without a click
- tray icon bumped to 18pt
2026-07-13 21:50:39 +05:30
Gagan
9b0776596d feat(x): resident app — launch at login + menu bar tray (Granola parity phase 1)
- Register as an OS login item on first packaged run (default on); the OS
  registry stays the source of truth afterward, with a toggle in
  Settings > Appearance > System
- Menu bar tray with template icon (derived from the app glyph, embedded
  as base64): Open Rowboat / Start-Stop meeting notes / Quit
- App keeps running with no windows while the tray exists; launches at
  login start hidden (menu bar only), Dock/tray reveals the window
- Tray menu reflects live recording state via meeting:setRecordingState;
  tray-initiated toggles survive window-loading races through a pending
  command drained on renderer mount (same pattern as deep links)
- GRANOLA_PARITY.md: research doc — how Granola works, current-state
  audit, gap analysis, phased parity plan
2026-07-13 21:02:20 +05:30
Gagan
aa47871e47 fix(watchers): ignore .git/node_modules in apps watcher, degrade watcher fd errors
The apps-dir watcher recursed into anything an installed app ships;
ignore .git and node_modules segments like the workspace watcher does.
(The skills watcher already bounds itself with depth: 1.)

fs.watch failures (EMFILE/ENOSPC) escape chokidar's error handler as
uncaught exceptions from Node watcher internals and killed the main
process with the native error dialog. Catch them at the process level
and log instead — watching is degradable; other uncaught exceptions
keep the default dialog behavior.
2026-07-13 18:16:12 +05:30
Gagan
5bf743f93d fix(workspace): pre-create watched roots so late-appearing dirs emit events
chokidar v4 silently skips a watched directory that doesn't exist at
watch time, so e.g. calendar_sync/ created by the first sync after app
start would never emit events until restart. Create the dir roots up
front; for the file root only its parent dir is needed.
2026-07-13 17:45:51 +05:30
Gagan
4db72e1fe4 fix(workspace): whitelist watched roots to stop EMFILE crash
The workspace watcher watched all of ~/.rowboat recursively. Chokidar v4
(no fsevents) holds one OS watch handle per file, and internal state dirs
(runs-archive, storage/turns, engines, ...) grow unboundedly with usage —
~20k open fds on a 1.1G workdir, crashing the app with EMFILE once the
fd limit is hit.

Only watch the roots whose change events actually have consumers:
knowledge, bases, inbox_lists, gmail_sync, calendar_sync, bg-tasks and
config/agent-schedule.json — with .git ignored (knowledge/ is a git repo).
2026-07-13 17:41:01 +05:30
Ramnique Singh
4182794b76
Merge pull request #745 from rowboatlabs/ai-sdk-7
chore(x): migrate to AI SDK 7
2026-07-13 16:50:06 +05:30
Ramnique Singh
cc2daf1874 chore(x): migrate to AI SDK 7
Bump apps/x from AI SDK v5 to v7 (single hop through the 6-0 and 7-0
migration guides). All deps upgraded via pnpm --filter:
ai@7, @ai-sdk/{anthropic@4,google@4,openai@4,openai-compatible@3,provider@4},
@openrouter/ai-sdk-provider@3, ollama-ai-provider-v2@4.

Changes (per the official migration guides):
- Provider spec bumped V2->V4: ProviderV2 -> ProviderV4 (models.ts, gateway.ts)
- streamText: result.fullStream -> result.stream (+ StreamTextInvoker seam,
  test fakes); stepCountIs -> isStepCount
- system -> instructions on SDK generateText/streamText/generateObject calls
  (left our own generateObjectSafe.system param untouched)
- allowSystemInMessages: true on calls passing stored messages (v7 rejects
  system-role messages in the array by default; convertFromMessages emits them)
- image content part {type:'image'} -> {type:'file'} (message-encoding.ts)
- tool() no longer accepts a `name` key (removed; tools keyed by ToolSet key)
- usage token relocation: cachedInputTokens -> inputTokenDetails.cacheReadTokens,
  reasoningTokens -> outputTokenDetails.reasoningTokens (mapUsage rewrite);
  renderer decoupled from LanguageModelUsage to a local flat UsageSummary
- providerMetadata relay cast to shared ProviderOptions (spec loosened)

No persisted-data changes: @x/shared is AI-SDK-free, so old chats
list/view/continue unchanged. Full typecheck clean; 637 tests pass
(shared 107, core 447, renderer 83); main.cjs bundles ESM-only ai into CJS.

Pending live check (needs creds): OpenAI strictJsonSchema now defaults true
in v7 and may reject zod schemas for openai-family flavors at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:22:36 +05:30
arkml
21bf4b985c
Merge pull request #739 from bglglzd/chore/maintenance-20260712-181414
Polish docs and comments in rowboat
2026-07-13 15:25:23 +05:30
Ramnique Singh
10525c58b8
Merge pull request #743 from rowboatlabs/fix/turn-lock-straggler-append
fix(x): hold the per-turn lock until straggler sync-tool appends land
2026-07-13 11:32:38 +05:30
Ramnique Singh
1dc042adc8 fix(x): hold the per-turn lock until straggler sync-tool appends land
A sync-tool batch runs under Promise.all, which rejects the moment one
tool's result append fails (a transient fs fault, or errorMessage()
throwing on a nullish thrown value). That rejection propagated out
through withLock and freed the per-turn lock while sibling tools were
still executing. A straggler's later append then ran with no lock held
and validated against a stale in-memory history rather than the file —
so if the caller re-advanced in between (writing the recovery
indeterminate result for the not-yet-settled tool), the straggler
appended a second tool_result for the same call. Duplicate results make
reduceTurn throw on every future read, permanently corrupting the turn
file and, through context references, every session behind it.

- executeAllowedTools now awaits Promise.allSettled and rethrows the
  first fault only after every tool has finished appending, so a fault
  never orphans a sibling.
- advance() drains the append queue (settleAppends) before withLock
  releases the turn, closing the fire-and-forget reportProgress path and
  any future append-path rejection.
- errorMessage() uses optional chaining so a `throw null`/`throw
  undefined` yields a normal isError tool result instead of a TypeError
  that escapes as an infrastructure rejection (the same fault that
  triggers the lock-escape mid-batch).

Adds two regression tests (both fail without the fix): a nullish throw
records an isError result and completes the turn; a faulted batch does
not settle until the slow sibling's append is durable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:17:42 +05:30
bglglzd
b8c3ea52a9 Small cleanup in rowboat 2026-07-12 21:14:14 +03:00
Prakhar Pandey
9bf286bfea feat: add provider Disconnect with automatic default reassignment
Each connected provider gets an explicit Disconnect in Settings. When the
disconnected provider was the default, another connected provider is
silently promoted (defaultSelection's provider preferred) through the
existing models:saveConfig path, and the toast names the new default.
Disconnecting the last provider clears the top-level config entirely so
llm:getDefaultModel degrades to the signed-in gateway default or a clean
no-config rejection and the composer shows its connect hint instead of a
phantom default. A defaultSelection pointing at the removed flavor is
dropped in the same write.
2026-07-12 23:10:08 +05:30
Prakhar Pandey
c9ebbd7600 feat: add search filter to model picker and live fallback for catalog providers
The picker dropdown gets a sticky search input filtering model ids across
all groups (headers hide at zero matches; loading/error rows stay visible
as status; a No-models-match row appears when nothing matches anywhere).
openai/anthropic/google groups fall back to a live models:listForProvider
fetch when the models.dev catalog is unavailable (signed-in mode or empty
cache), so they always show the provider's full list.
2026-07-12 23:10:08 +05:30
Prakhar Pandey
367071916a feat: aggregate model picker in composer grouped by connected provider
The composer picker lists every model across all connected providers:
models.dev catalog groups for openai/anthropic/google (plus the rowboat
gateway when signed in) and live per-provider fetches via
models:listForProvider for openrouter/aigateway/ollama/openai-compatible,
each group loading and failing independently with inline Retry. Picking a
model persists as the app default (models:updateConfig defaultSelection)
so background agents and new tabs follow the last pick; per-tab override
behavior is unchanged. Empty state points to Settings; the dropdown
scrolls for large lists and OpenRouter ids containing slashes are handled.
2026-07-12 18:50:49 +05:30
Prakhar Pandey
5377d7837a feat: make Settings > Models connect-only
Settings no longer exposes model selection. Each provider connects with
just its API key (or base URL for local providers); the live fetch now
drives a connection status line (Connected - N models) and the model the
config schema still requires is resolved silently at save (saved >
preferred > first fetched). Escape hatches: openai-compatible keeps a
visible Model field, and fetch failures offer manual entry so offline
saves still work. Provider cards show a Connected badge per configured
flavor. Save payload, models:test gate, and the four per-function fields
unchanged. Model selection moves to the chat composer next.
2026-07-12 14:14:55 +05:30
Prakhar Pandey
75805e2d67 feat: fetch provider models live from API key in settings
Settings > Models is now key-first, matching the onboarding flow: entering
an API key (or base URL for local providers) fetches the provider's live
model list via the existing models:listForProvider IPC, shown in a
dropdown. Free-text model entry survives only as the fetch-failure
fallback and the openai-compatible custom-model escape hatch. Unkeyed
providers no longer show auto-seeded models. Also fixes the models[] wipe:
editing the primary model previously discarded every saved model after
the first.
2026-07-12 13:36:16 +05:30
PRAKHAR PANDEY
2e45bc0213
Merge pull request #734 from prakhar1605/fix/skills-watcher-cold-start
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
fix: skills load without restart on first install
2026-07-11 21:46:05 +05:30
Prakhar Pandey
7ec93ba28f fix: watch skill roots before they exist so first install loads without restart 2026-07-11 14:04:31 +05:30
Ramnique Singh
2e7950182a
Merge pull request #733 from rowboatlabs/fix/builtin-error-envelope
fix(x): record builtin error envelopes as isError in durable results
2026-07-11 12:47:59 +05:30
Ramnique Singh
054f807925 fix(x): record builtin error envelopes as isError in durable results
Builtins signal failure by returning an envelope ({error}, {success:
false, error|message}, composio's {successful: false}) instead of
throwing, and the tool-registry bridge marked every non-throwing result
isError: false — so the durable log recorded most builtin failures as
successes, misleading the reducer's record, the renderer's error state,
telemetry, and the tools_extended gate. The spec's ToolResultData.isError
channel was effectively populated only by throws.

The bridge now detects the envelope at its single chokepoint and sets
isError accordingly. Model-facing bytes are unchanged (encoding ignores
isError). Deliberately not an error: executeCommand's success:false for
a plain non-zero exit with no error/message — a failing grep is a
result, not a tool failure. Composio success passthrough (error: null)
stays non-error via the truthy-string check. The authoring convention is
documented on the catalog schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:31:47 +05:30
Ramnique Singh
476c1792ff
Merge pull request #732 from rowboatlabs/fix/cancel-without-materialization
fix(x): cancellation no longer requires live dependency materialization
2026-07-11 11:13:38 +05:30
Ramnique Singh
49cb64d8ba fix(x): cancellation no longer requires live dependency materialization
advance() resolved the context chain, agent snapshot, live model, and
every snapshotted tool before applying ANY input — including cancel,
which uses none of them. A permanently broken environment (provider
removed from models.json, builtin renamed by an update, context
predecessor unreadable) therefore rejected every advance forever: the
turn could not be cancelled, stopTurn's fallback failed, and the
session rejected sendMessage with TurnNotSettledError permanently — a
bricked session with valid bytes on disk.

Materialization is now a thunk the invocation awaits only for work that
continues the turn: run() short-circuits a cancel input straight to the
cancel path (same commit ritual, same synthetic results, byte-identical
events for healthy turns), while every other input still materializes
first, preserving §15.3's reject-before-touching-the-file ordering.
applyInput narrows to non-cancel inputs.

Tests: cancel succeeds against a broken tool registry (with the
non-cancel input asserted to reject file-unchanged), a broken model
registry, and an unreadable context chain; plus a healthy-cancel
regression pinning the exact event sequence and outcome. All three
broken-environment tests fail against the old code. Design doc §15.3,
§18, §22, and §26.5 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:59:42 +05:30
arkml
30def374af
Merge pull request #707 from rowboatlabs/fix/outlook-quoted-replies
fix(x): collapse quoted replies in Outlook-authored email
2026-07-11 10:52:36 +05:30
Arjun
0e3918f32a fix(x): stop clipping the last line of short email bodies
A message body whose first block has a top margin (a bare <p>, most
Outlook mail) rendered with its final line cut off. The margin collapses
through <body>, so body.scrollHeight under-reports by the margin height
while the content's real bottom sits lower — and that short height is
what we set on the iframe. Measured in Electron: a two-line reply
reported 63px against a 73px content bottom, losing 10px. display:
flow-root on <body> contains the margin (the existing rule only reset the
*last* child's bottom margin). Pre-existing; not caused by quote
collapsing.

Also restore <body> attributes when re-serializing a prepared body.
prepareEmailHtml joined head.innerHTML + body.innerHTML, dropping the
<body> tag — and with it the bgcolor/style that 431 of 584 local
messages rely on for their background (the host parser merges a nested
<body>'s attributes onto its own). Serialize documentElement.innerHTML
instead so the tag and its attributes survive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 10:35:24 +05:30
Ramnique Singh
14f27b2d51
Merge pull request #731 from rowboatlabs/fix/sync-tool-result-append
fix(x): never record a repository failure as a tool's own error result
2026-07-11 10:33:50 +05:30
Ramnique Singh
46ad5aa3b6 fix(x): never record a repository failure as a tool's own error result
executeSyncTool's success-result append sat inside the try whose catch
appends an error tool result. A transient repository failure (disk
pressure, EMFILE) on that append was therefore durably recorded as the
TOOL having failed — after its side effect already happened — inviting
the model to retry the side effect on the next call (double-sent email,
duplicate issue). This violated §21.4: repository failures must reject
the execution, not masquerade as tool outcomes.

The try now covers execution and result parsing only (a tool returning
garbage is still a tool error). The success append happens outside: a
failure there propagates as an infrastructure error, the log keeps the
invocation without a result, and the existing §23 recovery closes it
honestly on re-advance ("outcome is unknown and it was not retried")
without re-executing.

Tests: a repo fake failing exactly the success append — asserts no
false error is persisted, the outcome rejects, recovery yields the
indeterminate result without re-execution, and a committed sibling
result survives untouched with correct model-facing ordering; plus a
parse-failure case pinning garbage returns as tool errors. Both repo
tests fail against the old code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:30:08 +05:30
Ramnique Singh
235f5802b9
Merge pull request #730 from rowboatlabs/fix/remove-dead-grant-buttons
fix(x): hide grant-scope buttons where no grant persistence exists
2026-07-11 00:05:04 +05:30
Ramnique Singh
8a41cd480a fix(x): hide grant-scope buttons where no grant persistence exists
The permission card offered "Allow for Session" and "Always Allow" on the
turns path (full-screen chat and side-pane), but the sessions layer never
persists grants — the scope rode respondToPermission metadata that nothing
consumes, so both buttons behaved exactly like "Approve" once while
telling the user otherwise. Grant persistence itself is deferred until
there is a sound notion of grant keys (executeCommand parsing is not
AST-based), and auto-permission mode covers the fatigue meanwhile.

The dropdown now renders only when the caller wires the scope handlers.
The legacy code-mode chat keeps them (runs:authorizePermission does
persist grants there); the turns-path call sites stop passing them and
drop the dead scope plumbing from their handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:55:07 +05:30
Ramnique Singh
7b01d1b404
Merge pull request #729 from rowboatlabs/fix/tool-permission-fail-closed
fix(x): fail-closed tool permissions with per-tool catalog declarations
2026-07-10 23:30:49 +05:30
Ramnique Singh
ac7679ec16 fix(x): fail-closed tool permissions with per-tool catalog declarations
The permission checker returned {required: false} for every non-builtin
toolId and for any builtin outside the executeCommand/file-tools switch,
so composio-execute-tool (email sends, GitHub/Jira writes), executeMcpTool,
and mcp:* attachments on user agents executed with no permission check —
even in manual mode — and never reached the auto-permission classifier.
The extension contract was inverted: a new side-effecting tool shipped
ungated unless someone remembered to extend a switch in another module.

Every builtin now declares its permission policy in the catalog itself
(required field, compile-enforced): "none", "prompt", "command-allowlist",
"file-boundary", "composio-execute", or "mcp-execute". The checker reads
the declaration and FAILS CLOSED: undeclared builtins, mcp:* attachments,
and unknown toolId families require permission. Composio and MCP requests
carry family-specific payloads (new ToolPermissionMetadata kinds, rendered
by the permission card; generic fallback for everything else). All
previously-ungated builtins keep today's behavior via explicit "none"
declarations, except addMcpServer which now prompts; a catalog test pins
the audited gated set so policy changes stay intentional.

Auto-permission flows (background tasks, live notes, channels) route the
newly gated calls through the existing classifier per the §9.3 matrix;
defer still denies without a human. No grant persistence yet — every
gated call prompts (or classifies) each time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:07:11 +05:30
Ramnique Singh
3b3380e8ed
Merge pull request #728 from rowboatlabs/fix/session-stop-concurrent-advances
fix(x): make stopTurn reliable under concurrent advances on one turn
2026-07-10 22:48:57 +05:30
Ramnique Singh
e22d3b4796 fix(x): make stopTurn reliable under concurrent advances on one turn
The sessions layer tracked live advances in a map keyed by bare turnId
with overwrite-on-set and unconditional delete-on-settle. A turn can
legally have several live advances at once — a running invocation plus
external-input invocations queued on the turn lock (e.g. a permission
response while an async result is pending). The overwrite made stopTurn
abort the wrong controller and await an outcome that couldn't settle;
the unconditional delete untracked a sibling advance, so a later
stopTurn missed the abort path, fell back to a cancel input, and could
reject with TurnInputError if the turn had completed meanwhile — a
user-visible stop failure. Durable state was never affected.

Track a set of advances per turn: each advance removes only its own
entry on settle, and stopTurn aborts every live controller and awaits
all outcomes. A cancel input that loses the race with a concurrent
settle now counts as a successful stop (the turn is already terminal)
instead of rejecting.

Adds four tests: concurrent invocations both aborted, an earlier settle
not untracking a later advance, the settle-race cancel treated as
success, and non-terminal rejections still surfacing. Updates
session-design.md §9.3 and §13.5 accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:46:06 +05:30
Ramnique Singh
284dc77f5e
Merge pull request #727 from rowboatlabs/fix/turn-commit-validation-gate
fix(x): validate turn event batches before they become durable
2026-07-10 22:39:31 +05:30
Ramnique Singh
21c93913f4 fix(x): validate turn event batches before they become durable
The commit ritual persisted first and reduced second, so an illegal
append (e.g. a misbehaving tool reporting progress after its terminal
result) became durable before the reducer rejected it — permanently
corrupting the turn file and, through context references, blocking
every later turn in the session. Reduce the batch against the in-memory
history first: illegal batches now reject in memory for their caller
only, the file stays legal, and a failed commit no longer poisons
this.events for subsequent appends in the same invocation.

Adds a test that fires a stashed reportProgress after the tool's result
is durable (while a sibling keeps the invocation alive) and asserts the
late append rejects, nothing illegal reaches the file/stream/bus, and
the turn completes and stays re-advanceable. Updates the design doc
(§4.5, §24) to the reduce → persist → stream order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:32:33 +05:30
Ramnique Singh
a1f03d0a69
Merge pull request #726 from rowboatlabs/runtime-refactor
fix(x): close out the runtime review — findings 4-10
2026-07-10 21:07:27 +05:30