chat: visibility model + integrations polish + templates sync + Hermes session fix#3
chat: visibility model + integrations polish + templates sync + Hermes session fix#3undeemed wants to merge 3 commits into
Conversation
…s session fix
Lands a bundle of related fixes from one user ask: auth leak on chat,
plain integration UI, connect-button reflow bug, missing composer
features, no templates auto-sync, no auto-scroll, and "every turn feels
like a new context" (Hermes session continuity).
## auth + chat visibility (PR1)
- agent_conversations gains optional `visibility` (personal|shared) +
`owner_user_id` + two indexes (by_owner_updated, by_visibility_updated)
- New `convex/lib/conversationAuth.ts` — `requireUser` /
`requireConversation` / `tryConversation` using the existing
`authComponent.safeGetAuthUser` pattern (not `ctx.auth.getUserIdentity`,
which isn't used anywhere in this repo)
- Every direct-id mutation/query that touches a conversation now goes
through the gate: `list`, `append`, `clear`, `rename`,
`delete`, `bindSession` in `agentMessages`; `startTurn`, `activeFor`,
`cancel`, `failFromRoute` in `agentTurns`. Client-passed `actor_slug`
is ignored.
- `bindSession` hardened with per-turn write_token + an
`expected_hermes_session` race guard — `setVisibility` re-mints the
Hermes session while a turn is mid-flight; the running wrapper's bind
no longer clobbers the fresh shared/personal id.
- Hermes session names: `castle-personal-<owner>-<rand>` vs.
`castle-shared-<rand>` so the FTS5 session store is partitioned by
visibility (no shared-chat content can bleed into a personal store).
- Hermes preamble passed per turn so the agent KNOWS the chat is shared
or personal and can self-moderate (e.g., refuse to repeat private
memory in a shared thread).
- Hermes wrapper now calls `agentMessages:bindSession` after minting a
new session — this was the root cause of "new context every turn":
load_session failed, mint_new succeeded, but the new id never got
patched back to Convex. Fixed at docker/hermes/serve.py:881.
- `convex/migrations.ts` — `stampDefaultsForConversations` (operator-
gated public mutation) + `claimMyUnownedConversations` (called
opportunistically on sidebar mount). Legacy actor_slug fallback in
canAccess is intentionally NOT used: same-local-part collision risk
(`sam@a.com`, `sam@b.com`).
- Sidebar split into Personal/Shared collapsible sections, each with
its own "+ new" button. Chat header shows visibility chip + "make
shared / make personal" toggle with confirm + fresh-memory-store
warning.
- Routes (`/api/agent/{start,cancel,regenerate}`) use
`fetchAuthMutation`/`fetchAuthQuery` so Convex sees the Better Auth
identity. Legacy `/api/agent/route.ts` (orphan, no callers) removed.
## integrations polish + composer (PR2)
- Right rail: real toolkit logos via Composio's `meta.logo` joined
from the cached catalog inside `listConnections`; status icon swapped
for filled check (connected) / pulsing dot (pending). Fallback chip
uses the first letter of the slug.
- Connect-button reflow killed: CTA only attaches once the latest
assistant message was created AFTER the agent_action — otherwise
the button would dangle below the previous assistant turn then jump
down when the new streaming response landed.
- One-shot scroll-to when a new CTA appears, even if user has scrolled
up.
- New `lib/use-chat-draft.ts` — per-conversation localStorage drafts
(key `castle:chat-draft:<id>`); hydrate on conversation switch,
persist on edit, clear on submit.
- Timestamps: surfaced on hover via a `<time>` element with relative
label + absolute tooltip. Auto-ticks every 60s.
- `regenerate` button on the last assistant message — new
`agentTurns.regenerateLast` deletes the old assistant message +
chunks + tool events, re-queues a fresh turn pointing at the same
user message and Hermes session. `/api/agent/regenerate` route kicks
Railway off again.
- `copy` button on assistant messages.
- File attachments (full flow):
- `agent_messages.attachments` schema field (storageId + name + ct)
- `agentMessages.generateAttachmentUploadUrl` mutation +
`attachmentUrl` query (Convex storage)
- Paperclip in the composer → upload to Convex storage → chips above
the textarea → on send, IDs go to `startTurn` which resolves URLs
and stuffs them into the kickoff body
- Hermes wrapper builds an `[the asker attached these files]`
preamble with name + URL per file (no OCR/preview — agent fetches
on demand)
- User messages render attachment chips with click-to-download links
## templates auto-sync (PR3)
- New `template_github_candidates` table — staging area, no
relaxation of the existing `templates` schema (required FKs to
customer + author stay intact; humans curate the promotion)
- `convex/templates.ts` — `listGithubCandidates`,
`upsertGithubCandidate` (idempotent; skips repos already linked OR
already on the staging table), `dismissGithubCandidate`,
`markCandidatePromoted`
- `/api/templates/sync-github` route accepts either an allowlisted
Better Auth session (manual "sync from GitHub" button) OR a Vercel
cron `Authorization: Bearer ${CRON_SECRET}` header. Uses
`CASTLE_SYSTEM_ACTOR_SLUG`'s GitHub Composio connection, falls back
to caller slug. Both POST and GET supported (Vercel cron uses GET).
- `vercel.json` cron entry — hourly, `0 * * * *`
- `<TemplateGithubCandidates>` section on `/templates` with "sync from
GitHub" button + per-row dismiss
## sticky auto-scroll (PR4)
- Track scroll distance via `scrollRef` listener with 80px threshold.
Only auto-scroll on new content if user is within 80px of the bottom.
- "↓ jump to latest" pill is sticky-inside the scroll container (not
absolute-positioned against the parent), so it can't collide with the
composer when the textarea is expanded.
## env vars needed in Vercel for full functionality
- `CRON_SECRET` — gates the hourly templates sync route
- `CASTLE_SYSTEM_ACTOR_SLUG` — actor whose Composio GitHub connection
the cron borrows
- `COMPOSIO_API_KEY` — already set; required for sync
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Deployment failed with the following error: Learn More: https://vercel.link/3Fpeeb1 |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThis PR implements a conversation ownership and visibility model, adds message attachments, introduces turn regeneration, syncs GitHub templates via daily cron, and refactors the chat UI to support personal/shared conversation lists with file attachments and turn regeneration controls. ChangesConversation Visibility, Message Attachments, and GitHub Template Sync
🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Vercel free plan only allows one cron job per day. Switch from `0 * * * *` (hourly) to `0 7 * * *` (daily at 07:00 UTC) so the deployment succeeds. Operators can still hit the "sync from GitHub" button on /templates for ad-hoc syncs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR delivers a connected set of fixes centred on conversation visibility/ownership, auth hardening, the Hermes
Confidence Score: 5/5Safe to merge; the auth model is well-structured and consistently applied, and the Hermes bindSession fix addresses a confirmed root cause. All direct-id queries and mutations now go through the new conversationAuth helpers, closing the old arbitrary-actor-slug vector. The bindSession hardening (write-token + expected_hermes_session race guard) is correct. The only new findings are a never-called check_canceled helper in serve.py that references the old activeFor signature, and a stale doc comment on listConversations — neither affects runtime behavior. docker/hermes/serve.py — the dead check_canceled function should either be removed or updated before the cancel-poll is re-enabled. Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as Browser / UI
participant VR as Vercel Route
participant CVX as Convex
participant HRM as Hermes (Railway)
Note over UI,HRM: Normal turn
UI->>VR: POST /api/agent/start
VR->>CVX: fetchAuthMutation agentTurns.startTurn
CVX-->>VR: turn_id, hermes_session, visibility
VR->>HRM: POST /agent/start with kickoff token
HRM-->>VR: 202
loop streaming
HRM->>CVX: agentMessageChunks:append
HRM->>CVX: agentTurns:heartbeat
end
alt new session minted
HRM->>CVX: agentMessages:bindSession
end
HRM->>CVX: agentTurns:complete
Note over UI,HRM: Regenerate
UI->>VR: POST /api/agent/regenerate
VR->>CVX: fetchAuthMutation agentTurns.regenerateLast
CVX-->>VR: turn_id, user_text, hermes_session
VR->>HRM: POST /agent/start same session
HRM-->>VR: 202
Note over UI,HRM: Cancel
UI->>VR: POST /api/agent/cancel
VR->>CVX: fetchAuthMutation agentTurns.cancel
VR->>HRM: POST /agent/cancel/turnId
Reviews (2): Last reviewed commit: "fix: apply CodeRabbit auto-fixes" | Re-trigger Greptile |
| * Until a row is claimed, the access check in | ||
| * `convex/lib/conversationAuth.ts` falls back to matching | ||
| * `actor_slug → caller.slug`, so existing chats remain accessible to | ||
| * their original users mid-migration. | ||
| * | ||
| * Run from CLI: | ||
| * npx convex run migrations:stampDefaultsForConversations |
There was a problem hiding this comment.
Migration comment contradicts actual
canAccess behavior
The comment says "Until a row is claimed, the access check falls back to matching actor_slug → caller.slug." In reality, canAccess() in conversationAuth.ts (line 64) returns false immediately for any row with no owner_user_id, with no slug fallback. Post-deploy, every legacy row without an owner_user_id will be forbidden — even to the original creator — until claimMyUnownedConversations runs for that user on sidebar mount. If that mutation is slow or skipped (e.g. direct-URL navigation before the sidebar mounts), the user sees a 403 on their own conversations. The comment should be corrected; the sidebar-mount call to claimMyUnownedConversations is the real recovery path.
| @@ -91,6 +200,36 @@ export const deleteConversation = mutation({ | |||
| }, | |||
| }); | |||
There was a problem hiding this comment.
deleteConversation leaves orphaned turns, chunks, and tool-events
The handler deletes agent_messages rows and the conversation itself but never touches agent_turns, agent_message_chunks, or agent_tool_events. All three tables have references (conversation_id on turns, turn_id on chunks/events) that become dangling after deletion. This accumulates unbounded orphan data over time and will confuse any future cron or query that sweeps those tables expecting live references.
| { | ||
| "path": "/api/templates/sync-github", | ||
| "schedule": "0 7 * * *" | ||
| } |
There was a problem hiding this comment.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
convex/agentMessages.ts (1)
184-199:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCascade turn state before deleting the conversation.
This only removes
agent_messagesand theagent_conversationsrow.agent_turns,agent_message_chunks, andagent_tool_eventsfor the same conversation remain orphaned, and a wrapper still finishing the turn can keep writing against rows whose assistant message has already been deleted. Please cancel/cascade the full turn graph here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/agentMessages.ts` around lines 184 - 199, The deletion only removes agent_messages and the agent_conversations row leaving agent_turns, agent_message_chunks, and agent_tool_events orphaned and possibly still writable; update the delete flow in the block after requireConversation(...) to cascade/cancel the entire turn graph for conversation id: query and mark any active agent_turns as cancelled/finished (so no background writer continues), then delete related rows in agent_message_chunks and agent_tool_events (and agent_messages if not already removed), and finally delete agent_turns and the agent_conversation row; use the existing ctx.db query/.withIndex/.collect and ctx.db.delete calls (same pattern as msgs and ctx.db.delete(id)) and perform these operations atomically/transactionally if supported so no leftover rows remain.lib/use-hermes-chat.ts (1)
182-233:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPropagate send failures back to callers.
sendMessagealways resolves after a failed/api/agent/startPOST and only stores the error locally. In this PR, the callers incomponents/chat-landing.tsxandcomponents/agent-panel.tsxclear their composer state immediately after invoking it, so a transient failure drops the user's unsent text and attachments. Return a success flag or rethrow here so callers only clear local state after the turn is actually created.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/use-hermes-chat.ts` around lines 182 - 233, sendMessage currently swallows failures and always resolves, causing callers to clear composer state on transient errors; modify sendMessage (the useCallback that calls fetch("/api/agent/start")) to return a success boolean (or throw) so callers can await it before clearing UI: ensure on a successful POST you return true, on non-OK response capture detail and call setSendError(...) then return false (or rethrow if you prefer), and in the catch block setSendError(...) and return false; keep setSending(false) in finally and update sendMessage's signature/return type so callers (e.g., components/chat-landing.tsx and components/agent-panel.tsx) can await the result before clearing text/attachments.
🧹 Nitpick comments (1)
components/chat-sidebar.tsx (1)
73-90: ⚡ Quick winMake the implicit auto-create visibility explicit.
This effect says it only auto-creates personal chats, but it's the one remaining
createConversationcall site that still relies on the backend default. Passingvisibility: "personal"here keeps the client-side contract stable if the mutation default changes later.Suggested change
- create({}).then((id) => onSelect(id)); + create({ visibility: "personal" }).then((id) => onSelect(id));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/chat-sidebar.tsx` around lines 73 - 90, The effect in useEffect implicitly creates a conversation with create({}) which relies on backend defaults; update the create call in the useEffect (the block that checks actorSlug, personal, shared, allConversations, selectedId and then calls create(...).then(...)) to pass the explicit visibility parameter, e.g. call create({ visibility: "personal" }) so the client-side contract doesn’t depend on server defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/templates/sync-github/route.ts`:
- Around line 93-101: The call to
c.tools.execute("GITHUB_LIST_ORGANIZATION_REPOSITORIES") currently ignores
result.successful and treats missing/invalid data as an empty list; update the
logic after the execute call (the result variable handling and repos assignment)
to check result.successful and, if false or missing expected data, fail the sync
by throwing an error or returning a non-200 Response (include the returned
result/error details in the message) instead of silently assigning repos = [];
ensure this guard is applied immediately after reading result so
GITHUB_LIST_ORGANIZATION_REPOSITORIES failures are surfaced.
In `@components/chat-landing.tsx`:
- Around line 245-255: The submit() path currently only checks
pendingAttachments and can send while uploads are in flight; update submit() to
also block when uploadingCount > 0 (i.e., require uploadingCount === 0) and
similarly disable/gate the send button UI so it cannot be clicked while
uploadingCount > 0; specifically, before calling sendMessage(text,
pendingAttachments.length ? pendingAttachments : undefined) in submit(), return
or await until uploadingCount === 0, and ensure the same check/guard is applied
to any other send handler referenced around the other send button code (the
alternate send handler you saw at 469-474) so clearDraft(),
setPendingAttachments([]), and sendMessage(...) only run once uploads are
finished.
In `@components/template-github-candidates.tsx`:
- Around line 105-109: The onClick handler for the button that calls dismiss({
id: c._id }) must handle rejection and show user feedback; wrap the async call
inside a try/catch inside the onClick (the handler that currently awaits dismiss
and then calls toast.success), call toast.success on success and toast.error
with a helpful message in the catch branch, and ensure any state used for
optimistic UI or disabling (if present) is restored in the failure path so there
are no unhandled promise rejections from the dismiss call.
In `@convex/agentMessages.ts`:
- Around line 308-313: The attachmentUrl query currently only calls requireUser
and returns ctx.storage.getUrl(storageId), allowing anyone with a storageId to
mint URLs; change the query signature (attachmentUrl) to accept a
conversation_id (or message_id) in args, call requireConversation(ctx,
conversation_id) (or requireMessage if available) to re-check
access/authorization before calling ctx.storage.getUrl(storageId), and only
return the signed URL after the conversation/message authorization succeeds;
update any related callers to pass the conversation_id/message_id.
- Around line 211-229: The handler for changing conversation visibility (handler
in this file, which calls requireConversation and updates via ctx.db.patch)
currently remints the session but allows in-flight turns to continue against the
old Hermes session; reject visibility flips while a turn is active by querying
the agent turns for this conversation (e.g. look up active/queued/running turns
associated with id) before computing newOwner/hermesSessionName and applying
ctx.db.patch, and throw a descriptive error (e.g. "cannot change visibility
while a turn is active") if any active turn exists; keep the existing logic for
allowed flips when no active turns are found.
In `@convex/agentTurns.ts`:
- Around line 350-361: The regenerate path currently only returns user_text from
the reused user message, losing attachments; update the object returned in the
function that builds the regenerated turn (the code using latest.user_message_id
and userMsg from ctx.db.get) to include the same attachment metadata/URLs fields
that startTurn returns (pull attachments from userMsg?.attachments or equivalent
and include them in the returned payload alongside user_text, user_message_id,
assistant_message_id, hermes_session, visibility, actor_slug, and turn_id) so
Hermes can forward attachments for regenerations.
In `@convex/templates.ts`:
- Around line 46-57: upsertGithubCandidate lowercases github_repo before lookups
but setGithubRepo persists owner/repo with original casing, causing misses;
update setGithubRepo to normalize the repo the same way upsertGithubCandidate
does (e.g., apply the existing normalize/lowercase helper) before writing
github_repo to the templates record so the templates table and
template_github_candidates use identical lowercased repo values for dedupe and
lookups.
- Around line 23-83: These handlers (listGithubCandidates,
upsertGithubCandidate, dismissGithubCandidate, markCandidatePromoted) are
public; add operator-only auth or make them internal and expose authenticated
wrappers: either insert an auth check at the top of each handler (e.g., call
ctx.auth.getUserIdentity(), validate identity/role and throw if unauthorized)
before any DB access, or convert the implementations to
internalQuery/internalMutation and create thin exported query/mutation wrappers
that call the internal versions only after verifying ctx.auth.getUserIdentity()
and operator permissions; ensure all four functions reference the same auth
logic so only authorized operators can read/modify template_github_candidates.
In `@docker/hermes/serve.py`:
- Around line 647-649: The current agent_start incoming-params validation
rejects falsy hermes_session, which breaks first-turn flows because
_run_turn_to_convex can mint a new session; update the validation so
hermes_session is not required to be truthy (e.g., remove hermes_session from
the all([...]) check or only validate other required fields turn_id,
conversation_id, actor_slug, write_token, convex_url) so agent_start accepts
empty strings for hermes_session and lets _run_turn_to_convex handle session
creation.
---
Outside diff comments:
In `@convex/agentMessages.ts`:
- Around line 184-199: The deletion only removes agent_messages and the
agent_conversations row leaving agent_turns, agent_message_chunks, and
agent_tool_events orphaned and possibly still writable; update the delete flow
in the block after requireConversation(...) to cascade/cancel the entire turn
graph for conversation id: query and mark any active agent_turns as
cancelled/finished (so no background writer continues), then delete related rows
in agent_message_chunks and agent_tool_events (and agent_messages if not already
removed), and finally delete agent_turns and the agent_conversation row; use the
existing ctx.db query/.withIndex/.collect and ctx.db.delete calls (same pattern
as msgs and ctx.db.delete(id)) and perform these operations
atomically/transactionally if supported so no leftover rows remain.
In `@lib/use-hermes-chat.ts`:
- Around line 182-233: sendMessage currently swallows failures and always
resolves, causing callers to clear composer state on transient errors; modify
sendMessage (the useCallback that calls fetch("/api/agent/start")) to return a
success boolean (or throw) so callers can await it before clearing UI: ensure on
a successful POST you return true, on non-OK response capture detail and call
setSendError(...) then return false (or rethrow if you prefer), and in the catch
block setSendError(...) and return false; keep setSending(false) in finally and
update sendMessage's signature/return type so callers (e.g.,
components/chat-landing.tsx and components/agent-panel.tsx) can await the result
before clearing text/attachments.
---
Nitpick comments:
In `@components/chat-sidebar.tsx`:
- Around line 73-90: The effect in useEffect implicitly creates a conversation
with create({}) which relies on backend defaults; update the create call in the
useEffect (the block that checks actorSlug, personal, shared, allConversations,
selectedId and then calls create(...).then(...)) to pass the explicit visibility
parameter, e.g. call create({ visibility: "personal" }) so the client-side
contract doesn’t depend on server defaults.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2208d784-6401-4c1a-9f63-1e909f8d8cbe
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (23)
app/api/agent/cancel/route.tsapp/api/agent/regenerate/route.tsapp/api/agent/route.tsapp/api/agent/start/route.tsapp/api/templates/sync-github/route.tsapp/connections/actions.tsapp/templates/page.tsxcomponents/agent-panel.tsxcomponents/chat-landing.tsxcomponents/chat-sidebar.tsxcomponents/connections-rail.tsxcomponents/template-github-candidates.tsxconvex/agentMessages.tsconvex/agentTurns.tsconvex/lib/conversationAuth.tsconvex/migrations.tsconvex/schema.tsconvex/templates.tsdocker/hermes/serve.pylib/chat-context.tsxlib/use-chat-draft.tslib/use-hermes-chat.tsvercel.json
💤 Files with no reviewable changes (1)
- app/api/agent/route.ts
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 9 file(s) based on 9 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 9 file(s) based on 9 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
Superseded by combined PR |
CodeRabbit (critical): - Block setVisibility while a turn is active — without it, a flip during a running turn could publish a personal-memory response into a now-shared thread (operator stops/waits for the turn instead) - attachmentUrl now takes message_id + storageId, gates via tryConversation, and verifies storageId actually belongs to that message. Stops holders of an orphan storageId minting URLs after a shared chat is demoted to personal - Hermes wrapper no longer rejects empty hermesSession — _run_turn_to_convex already treats it as "mint new + bind back," so the previous 400 blocked first-turn flows on brand-new conversations CodeRabbit (major): - regenerateLast now resolves and returns attachment URLs alongside user_text, and the regenerate route forwards them in the kickoff body — without this, regenerating a user message with attached files re-prompted Hermes with text only - Composer submit + send button are gated on uploadingCount === 0; sending mid-upload would have stranded files in the composer for the next turn - normalizeGithubRepo() shared helper now used by both upsertGithubCandidate and setGithubRepo so `Owner/Repo` and `owner/repo` dedupe correctly across the candidates+templates tables CodeRabbit (minor): - Dismiss handler on template candidates wraps the mutation in try/catch + toast.error so failures aren't an unhandled rejection Greptile (P1 security): - listGithubCandidates, dismissGithubCandidate, markCandidatePromoted now require an authenticated user — NEXT_PUBLIC_CONVEX_URL is public, so without these gates anyone could enumerate or wipe staged candidates. upsertGithubCandidate stays open so the Vercel cron (which has no operator identity) can still write - Migration docstring rewritten to match actual canAccess behavior (legacy actor_slug fallback was deliberately removed; recovery is via opportunistic claim, now hoisted to ChatProvider so it runs on every page, not just /) Greptile (P2): - deleteConversation now cascade-deletes agent_turns, agent_message_chunks, and agent_tool_events — previously these became unbounded orphan data Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Bundles a connected set of fixes from one user ask.
Auth + chat visibility
agent_conversationsgainsvisibility(personal|shared) +owner_user_id+ indexesauthComponent.safeGetAuthUserbindSessionhardened with per-turn write-token +expected_hermes_sessionrace-guardsetVisibilityrefuses mid-turn (the running Hermes task is bound to the OLD session via its kickoff payload; re-minting under it could leak personal-memory into a now-shared thread)castle-personal-<owner>-…vscastle-shared-…); visibility-aware system preamblebindSessionback to Convex when minting a new session — was the root cause of "new context every turn"/)deleteConversationnow cascade-deletes turns, chunks, and tool events (no more orphan rows)Integrations + composer polish
regenerate(new/api/agent/regenerate+agentTurns.regenerateLast) re-runs the last turn against the same Hermes session — now carries the original attachments forward tooattachmentUrlgated bymessage_id+ verifies the storageId is actually on that message (defense in depth: a caller with access to convo A can't pass A's id but ask for B's storageId)Templates auto-sync
template_github_candidatesstaging table (no relaxation oftemplates)/api/templates/sync-githubaccepts both Better Auth sessions and Vercel cron'sCRON_SECRETbearer headerlistGithubCandidates/dismissGithubCandidate/markCandidatePromoted;upsertGithubCandidatestays open so the cron (no operator identity) can writevercel.jsoncron:0 7 * * *— daily, not hourly (Vercel Hobby tier caps at one cron job per day)/templateswith manual "sync from GitHub" buttonnormalizeGithubRepohelper so candidates and templates dedupe consistently (Owner/Repoandowner/repocollapse)Sticky auto-scroll
Env vars needed in Vercel
CRON_SECRET— gates the daily templates syncCASTLE_SYSTEM_ACTOR_SLUG— actor whose Composio GitHub connection the cron borrowsCOMPOSIO_API_KEY— required for sync (already set)Post-deploy
Run once:
npx convex run migrations:stampDefaultsForConversationsto backfillvisibility = "personal"on legacy rows.Test plan
🤖 Generated with Claude Code