Track block YAML gains optional `model` and `provider` fields. When set,
the track runner passes them through to `createRun` so this specific
track runs on the chosen model/provider; when unset the global default
flows through (`getTrackBlockModel()` + the resolved provider).
The track skill picks up the new fields automatically via the embedded
`z.toJSONSchema(TrackBlockSchema)` and adds an explicit "Do Not Set"
section: copilot leaves them omitted unless the user named a specific
model or provider for the track. Common bad reasons ("might be faster",
"in case it matters", complex instruction) are called out so the
defaults stay the path of least resistance.
Track modal Details tab shows the values when set, in the same
conditional `<dt>/<dd>` style as the lastRun fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring back per-category model selection that 5c4aa772 dropped, plus add a
new track-block category. Each is a BYOK-only override on `LlmModelConfig`
(`knowledgeGraphModel`, `meetingNotesModel`, `trackBlockModel`); signed-in
users always get the curated gateway default and never hit the on-disk
config.
Three helpers in core/models/defaults.ts — `getKgModel`,
`getTrackBlockModel`, `getMeetingNotesModel` — each check `isSignedIn`
first (fast path) and fall through to `cfg.<field> ?? cfg.model` for BYOK.
The model is now picked at the invocation site rather than via runtime
agent-name branching: each top-level `createRun` for a polling KG agent
or a track-block update passes `model: await getXxxModel()`. The `model:`
declarations on the affected agent YAMLs are dropped — they were dead
code under the per-call override. Standalone (non-run) callers
`track/routing` and `summarize_meeting` use the helpers inline.
Settings dialog and the two onboarding flows surface the two new fields
("Meeting Notes Model", "Track Block Model") next to the existing
"Knowledge Graph Model"; `repo.setConfig` persists all three per-provider.
Note: the signed-in `RowboatModelSettings` panel still has its
now-defunct kg selector; that's a UI cleanup for a later pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes polling schedules from the up-next and calendar track blocks on
Today.md so they refresh only on calendar.synced events, and rewrites
the emails track instruction to consume a multi-thread digest payload.
Batches Gmail sync so one email.synced event covers a whole sync run
(capped at 10 threads per digest) instead of one event per thread,
which collapses Pass 1 routing calls for multi-thread syncs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The model dropdown was broken in two ways: it wrote to ~/.rowboat/config/models.json
(the BYOK creds file, stamped with a fake `flavor: 'openrouter'` to satisfy zod
when signed in), and the runtime ignored that write entirely for signed-in users
because `streamAgent` hard-coded `gpt-5.4`. Model selection was also globally
scoped, so every chat shared one brain.
This change moves model + provider out of the global config and onto the run
itself, resolved once at runs:create and frozen for the run's lifetime.
## Resolution
`runsCore.createRun` resolves per-field, falling through:
run.model = opts.model ?? agent.model ?? defaults.model
run.provider = opts.provider ?? agent.provider ?? defaults.provider
A new `core/models/defaults.ts` is the only place in the codebase that branches
on signed-in state. `getDefaultModelAndProvider()` returns name strings;
`resolveProviderConfig(name)` does the name → full LlmProvider lookup at
runtime. `createProvider` learns about `flavor: 'rowboat'` so the gateway is
just another flavor.
`provider` is stored as a name (e.g. `"rowboat"`, `"openai"`), not a full
LlmProvider object. API keys never get written into the JSONL log; rotating a
key in models.json applies to existing runs without re-creation. Cost: deleting
a provider from settings breaks runs that referenced it (clear error surfaced
via `resolveProviderConfig`).
## Runtime
`streamAgent` no longer resolves anything — it reads `state.runModel` /
`state.runProvider`, looks up the provider config, instantiates. Subflows
inherit the parent run's pair, so KG / inline-task subagents run on whatever
the main run resolved to at creation. The `knowledgeGraphAgents` array,
`isKgAgent`, and the per-agent default constants are gone.
KG / inline-task / pre-built agents declare their preferred model in YAML
frontmatter (claude-haiku-4.5 / claude-sonnet-4.6) — used at resolution time
when those agents are themselves the top-level agent of a run (background
triggers, scheduled tasks, etc.).
## Standalone callers
Non-run LLM call sites (summarize_meeting, track/routing, builtin-tools
parseFile) and `agent-schedule/runner` were branching on signed-in
independently. They all route through `getDefaultModelAndProvider` +
`resolveProviderConfig` + `createProvider` now; `agent-schedule/runner`
switched from raw `runsRepo.create` to `runsCore.createRun` so resolution
applies to scheduled-agent runs too.
## UI
`chat-input-with-mentions` stops calling `models:saveConfig`. The dropdown
notifies the parent via `onSelectedModelChange` ({provider, model} as names);
App.tsx stashes selection per-tab and passes it to the next `runs:create`.
When a run already exists, the input fetches it and renders a static label —
model can't change mid-run.
## Legacy runs
A lenient zod schema in `repo.ts` (`StartEvent.extend(...optional)` plus
`RunEvent.or(LegacyStartEvent)`) parses pre-existing runs. `repo.fetch` fills
missing model/provider from current defaults and returns the strict canonical
`Run` type. No file-rewriting migration; no impact on the canonical schema in
`@x/shared`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
streamAgent executed tools with no try/catch around the call. A throw
from execTool or from a subflow agent streamed up through streamAgent,
out of trigger's inner catch (which rethrows non-abort errors), and
into the new top-level catch that the previous commit added. That
surfaces the failure — but it ends the run. One misbehaving tool took
down the whole conversation.
Wrap the tool-execution block in a try/catch. On abort, rethrow so the
existing AbortError path still fires. On any other error, convert the
exception into a tool-result payload ({ success: false, error, toolName })
and keep going. The model then sees a tool-result message saying the
tool failed with a specific message and can apologize, retry with
different arguments, pick a different tool, or explain to the user —
the normal recovery moves it already knows how to make.
No change to happy-path tool execution, no change to abort handling,
no change to subflow agent semantics (subflows that themselves error
are treated identically to regular tool errors at the call site).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AgentRuntime.trigger() wrapped its body in try/finally with no outer
catch. An inner catch around the streamAgent for-await only handled
AbortError and rethrew everything else. Call sites fire-and-forget
trigger (runs.ts:26,60,72), so any thrown error became an unhandled
promise rejection. The finally still ran and published
run-processing-end, but nothing told the renderer why — the chat
showed the spinner, then an empty assistant bubble.
Provider misconfig, invalid API keys, unknown model ids, streamText
setup throws, runsRepo.fetch or loadAgent failing, and provider
auth/rate-limit rejections on the first chunk all hit this path on a
first message. All invisible.
Add a top-level catch that formats the error to a string and emits a
{type: "error"} RunEvent via the existing runsRepo/bus path. The
renderer already renders those as a chat bubble plus toast
(App.tsx:2069) — no UI work needed.
No changes to the abort path: user-initiated stops still flow through
the existing inner catch and the signal.aborted branch that emits
run-stopped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concurrent track runs on the same note were corrupting the file. In a
fresh workspace, four tracks fired on cron at 05:09:17Z (all failed on
AI_LoadAPIKeyError, but each still wrote lastRunAt/lastRunId before the
agent ran) and three more fired at 05:09:32Z. The resulting Today.md
ended with stray fragments "\n>\nes-->\n-->" — tail pieces of
<!--/track-target:priorities--> that a mis-aimed splice had truncated —
and the priorities YAML lost its lastRunId entirely.
Two compounding issues in knowledge/track/fileops.ts:
1. updateTrackBlock read the file twice: once via fetch() to resolve
fenceStart/fenceEnd, and again via fs.readFile to get the bytes to
splice. If another writer landed between the reads, the line indices
from read #1 pointed into unrelated content in read #2, so the
splice replaced the wrong range and left tag fragments behind.
2. None of the mutators (updateContent, updateTrackBlock,
replaceTrackBlockYaml, deleteTrackBlock) held any lock, so
concurrent read-modify-writes clobbered each other's updates. The
missing lastRunId was exactly that: set by one run, overwritten by
another run's stale snapshot.
The fix: introduce withFileLock(absPath, fn) in knowledge/file-lock.ts,
a per-path Promise-chain mutex modeled on the commitLock pattern in
knowledge/version_history.ts. Callers append onto that file's chain
and await — wait-queue semantics, FIFO, no timeout. The map self-cleans
when a file's chain goes idle so it stays bounded across a long-running
process.
Wrap all four fileops mutators in it, and also wrap workspace.writeFile
(which can touch the same files from the agent's tool surface and
previously raced with fileops). Both callers key on the resolved
absolute path so they share the same lock for the same file.
Reads (fetchAll, fetch, fetchYaml) stay lock-free — fs.writeFile on
files this size is atomic enough that readers see either pre- or
post-state, never corruption, and stale reads are not a correctness
issue for the callers that use them (scheduler, event dispatcher).
The debounced version-history commit in workspace.writeFile stays
outside the lock; it's deferred work that shouldn't hold up the write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire a custom fetch into the OpenRouter gateway provider so each outbound
request resolves a fresh access token, instead of baking one token into
the provider at turn start. Add a 60s expiry margin and serialize
concurrent refreshes behind a single in-flight promise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Added a function to invalidate the Copilot instructions cache when setting the API key.
- Updated the Composio tools prompt to return an empty string if Composio is not configured, simplifying the user experience.
- Refactored the Copilot instructions to conditionally include Composio-related guidance based on configuration status, improving clarity on third-party service interactions.
- Introduced a new function to build a skill catalog string, allowing for optional exclusion of specific skills, enhancing the skill management capabilities.
Track blocks are YAML-fenced sections embedded in markdown notes whose output
is rewritten by a background agent. Three trigger types: manual (Run button or
Copilot), scheduled (cron / window / once with a 2 min grace window), and
event-driven (Gmail/Calendar sync events routed via an LLM classifier with a
second-pass agent decision). Output lives between <!--track-target:ID-->
comment markers that render as editable content in the Tiptap editor so users
can read and extend AI-generated content inline.
Core:
- Schedule and event pipelines run as independent polling loops (15s / 5s),
both calling the same triggerTrackUpdate orchestrator. Events are FIFO via
monotonic IDs; a per-track Set guards against duplicate runs.
- Track-run agent builds three message variants (manual/timed/event) — the
event variant includes a Pass 2 directive to skip updates on false positives
flagged by the liberal Pass 1 router.
- IPC surface: track:run/get/update/replaceYaml/delete plus tracks:events
forward of the pub-sub bus to the renderer.
- Gmail emits per-thread events; Calendar bundles a digest per sync.
Copilot:
- New `tracks` skill (auto-generated canonical schema from Zod via
z.toJSONSchema) teaches block creation, editing, and proactive suggestion.
- `run-track-block` tool with optional `context` parameter for backfills
(e.g. seeding a new email-tracking block from existing synced emails).
Renderer:
- Tiptap chip (display-only) opens a rich modal with tabs, toggle, schedule
details, raw YAML editor, and confirm-to-delete. All mutations go through
IPC so the backend stays the single writer.
- Target regions use two atom marker nodes (open/close) around real editable
content — custom blocks render natively, users can add their own notes.
- "Edit with Copilot" seeds a chat session with the note attached.
Docs: apps/x/TRACKS.md covers product flows, technical pipeline, and a
catalog of every LLM prompt involved with file+line pointers.
* feat(oauth): enhance Rowboat sign-in process to prevent duplicate users
Added billing information checks during the Rowboat OAuth connection and onboarding process to ensure user and Stripe customer existence before proceeding. This change mitigates the risk of creating duplicate users due to parallel API calls. Updated error handling for better debugging in case of failures.
* refactor(onboarding): remove billing info check during Rowboat OAuth connection
Eliminated the billing information check that was previously in place to prevent duplicate Stripe customers during the onboarding process. This change simplifies the onboarding flow while maintaining the necessary checks for composio flags after account connection.
Cmd+K (Ctrl+K on Win/Linux) now opens a unified palette with two
modes: Chat (default) and Search (existing behavior). Tab cycles
between them. In Chat mode, if the user triggered the shortcut from
the markdown editor, the palette auto-attaches a removable chip
showing the note path and precise cursor line. Enter sends the
prompt to the right-sidebar copilot — opening the sidebar if closed
and starting a fresh chat tab — with the chip carried as a
FileMention whose lineNumber is forwarded to the agent as
"... at <path> (line N)" so the agent can use workspace-readFile
with offset to fetch the right slice on demand.
Line numbers are computed against the same getMarkdownWithBlankLines
serializer used to write notes to disk, so the reference is
byte-identical to what the agent reads back.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mentions now route through the structured-attachment path, sending
only path/filename/mimeType. The agent fetches content on demand via
workspace-readFile (line-prefixed, paginated). Avoids freezing a stale
snapshot of the note into the conversation and saves tokens on long
notes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Returns utf8 reads as `<path>`/`<type>`/`<content>` blocks with each
line prefixed by its 1-indexed line number, plus offset/limit paging
and an end-of-file/truncation footer. Helps the agent reference
specific lines when forming precise edits to knowledge markdown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>