agentfeat(agents): four namespaces, a default file, and an agent doc that opens as the agent An agent's folder now reads as four namespaces — playbook, config/, memory/ and everything else as output — by CLASSIFICATION rather than migration, so not one document row moves. The thing that actually writes these paths is prompt text, some of it stored in seeded agent bodies, and `overview` is addressed by exact-path equality while `archives/` is matched by a metadata-stamping regex; re-pointing the helpers would have been a silent-failure change. This generalises the split the coaching agent already ships. `default_file` frontmatter says which file the Files tab opens. Relative to the agent's folder so it survives duplicate and publish, resolved once on the server so no client re-derives the default, absent meaning `overview` so nothing changes on deploy. Set from the agent's menu, a file row, `agent.create` or the CLI. The frontmatter patcher learned to REMOVE a key, which is what makes "back to the default" expressible at all. A document whose type is `agent` renders as the agent. Five surfaces rendered one as prose and only one mounted the structured header, so the raw YAML showed with no header at all on the rest — and the hider was looking for a horizontal-rule run when the server emits a `frontmatter` sentinel code block. Both are fixed: the render is keyed on documentType, and the settings are a collapsed form above the editor carrying every key, with unknown ones kept in their own block rather than dropped. Brain documents share one measure and one left edge: the reading column drops to 80ch with no horizontal padding, its back gutter is resized to match, and a stale `ml-4` on the playbook crumb is gone. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 2, 2026, 11:51 PMboardfeat(mail): the inspector is the document, at the reader's width Six things, all of them the panel arguing with the person reading it. **It is the real document now.** The body was a read-only markdown render of `doc.content`. It is `<Document />` bound to the same Y.Doc the document page binds, through the same provider registry — so it is not a copy that could drift, it is the document, and typing in the panel and typing on the page are the same keystrokes. No merge to worry about: two views of one CRDT is the case Y.js exists for. **The width is the reader's.** A profile is sometimes the thing you came for and sometimes an aside, and no fixed width is right for both. The left edge is a drag handle, 320–900px, remembered per viewer. Default 480, up from 416. **The title is a heading.** It was `text-sm font-medium` — the same type as the rows under it — so the panel read as a list whose first line happened to be a name. The breadcrumb above it is gone. **No fill behind the properties.** One surface per object: the panel is the surface, and a filled block inside it was a second box drawn around one thing. **Every declared field is a row, value or not.** A schema declares a field because somebody wants that fact recorded, so an empty one is a prompt, not an absence — hiding it means the only way to discover the graph wants a title is to already know. **An array is its values, not its JSON.** Agents write `buying_role` both as `['economic_buyer','finance_approver']` (a tool call) and as `'economic_buyer, finance_approver'` (a markdown round-trip). `toFieldValue` JSON-stringified the first, so the same field rendered as a literal `["economic_buyer","finance_approver"]` in one graph and as two chips in another. Arrays of scalars now take the comma-joined form `splitMultiSelect` already parses, so both shapes render as chips. **A select opens in a popover.** The inline version spliced every option into the form, so opening one field pushed every field below it down the panel and a five-option field was the tallest thing on screen. It is an `OptionPicker` now — filter, ordinals, right-hand tick, keyboard containment, all of it already built. A multi-select stays open across toggles, since picking two values should not be two openings. Also drops the `as any` in the floating-edge test: `getFloatingEdgeParams` takes the two fields it actually reads (`NodeBox`) instead of the whole 20-field `InternalNode`, which a test could only fabricate with a cast — and a cast is what would stop the compiler noticing the day React Flow renames `positionAbsolute`. Co-Authored-By: Claude Opus 5 <<email>>Sep 20, 2026, 4:13 PMcoachingfeat(documents): play the call inside the document, at the moment being discussed A coaching note the rep cannot check against the tape is an assertion, and until now checking it meant leaving the document for a provider page that opened at 0:00. A ```moment card now embeds the call positioned at `at:`, every timestamp in its transcript is a button that seeks the player to that line, and a new ```recording fence embeds a whole call anywhere in a document. Playback resolves from `externalId`, never a stored URL. That is not a preference: Circleback's `recording_url` is a signed link that expired 24h after ingest, so every Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a web page rather than media. The id is stable, and the server mints from it per view. Cedar's own <video> rather than a provider embed, because only an element we own can be seeked — verified against the live providers rather than assumed. Fathom's /embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share), but its component declares only {call, autoplay, shareUrl, displayMode} and nine probed parameter names changed nothing, so it cannot be positioned; it is kept as the last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125 comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari. Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media, and the tenant's real call URL read off metaData.url instead of the hardcoded us-4796 subdomain. Its 403 returns `permission_required`, deliberately not `unavailable` — the recording exists and Cedar may not fetch it, which is a problem with a named owner and a one-minute fix, so the player names the api:calls:read:media-url scope and where a Gong admin grants it. The connection form now asks for it up front. This lives in gong-recording.ts, not gong.ts, because gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and importing the minter from there crashes on load with a TDZ error. Nothing mints until the reader presses play: Fathom allows 30 download requests per 60s per key and renders a cold recording in ~34s, so a document with ten moments that minted eagerly would spend a third of that budget before anyone pressed anything. Not verified, and called out in the design doc: no live mint ran (this machine's ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path has never run against Gong since no org has a connection, and whether a seek lands on the right words is still unproven — Fathom's call started_at and recording.started_at differ by 62s on the one sample inspected. Also carries a pre-existing working-tree change to fathom.ts that could not be separated from the same file: transcript turns keep their timestamp, so the coaching rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is included rather than left orphaned. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMfile-linkfix(playbook): the four things wrong with the trigger UI 1. TWO GLYPHS THAT SAID NOTHING. The meeting glyph was invisible. It asked `connectionLogo('meetings')`, which resolves to the hand-drawn icons.tsx Calendar — an `<svg fill="none">` whose path sets no fill of its own, so it inherits `none` and paints NOTHING unless the call site passes a `fill-*` class. That row passes only a size. Fixed at both ends: the glyph row now uses lucide's Calendar (which carries its own stroke), and the shared icon gets `fill="currentColor"` on its path so every other call site that passes only a size renders too. It is a presentation attribute, which a CSS class beats, so date-picker-dialog's `fill-[#9D9D9D]` is unaffected. The external-CRM glyph was sync arrows, which read as "refresh" — an action, not an event — and named neither the event nor the system. Both marks now come from the conversation timeline (Calendar, Building2), which is where a user has already learned what these events look like. The trigger badge follows suit. 2. THE MEMBER PICKER MOVES INTO THE BREADCRUMB ROW. It answers the same question the crumbs do — WHOSE playbook is this — so on its own row underneath it read as a filter over the content rather than as part of the address, and cost a row of height on every load for a control most sessions never touch. Only the picker moves: the banner and the permission notice stay in AdministeredUserBar, because they are full-width messages and squeezing either into a title row would truncate it or shove the crumbs sideways as it appears. The bar now collapses to nothing when it holds neither. 3. A REF WITH NO INSTRUCTION HAD NO WAY TO GET ONE. Every ref authored before this feature is self-closing, so it parses to the inline chip — and the chip had nowhere to put an instruction. That is not a migration to wait out; it is every ref in every playbook today. The chip now carries a second half, `+ Instructions`, which converts it into the panel with the caret already in the instruction area. Offered ONLY inside a trigger: a ref in <always-loaded> is a document the agent reads, not an agent a trigger fires, so there is no trigger for it to have an instruction for. The shared FileLinkNode does not learn what a trigger is — it asks its host "does anyone want a segment on this chip", and the playbook is the only host that answers. The conversion carries `section`/`when` across (dropping them would turn "add an instruction" into "quietly widen this ref's scope") and has two shapes: a chip alone in its paragraph replaces the paragraph, leaving no empty one behind; a chip among prose leaves the prose as the block's own note. 4. THE DISPATCH EXPLAINER MOVES INTO A `?`. It was a line under the pill. But it is REFERENCE, not status — identical for every trigger of a kind, never changing — and a sentence that never changes on a row you read daily stops being read within a week while still costing a line of height on every block in the document. Behind a `?` it is one click away on the day you need it and invisible on the days you do not. The popover answers two things in the order they are needed: what wakes it (with the four event glyphs when the trigger is `any`, since "every event" is the one label that names no thing), and what happens then — whether an orchestrator reads this block and CHOOSES, or every agent in it simply runs. 4,103 mail tests green; tsc -b clean. Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 11:00 PMgraphfix(graphs): a deal's org chart is readable by the people on that deal Greptile's remaining open finding on this PR. A conversation chart lives at `conversation/{id}/org-chart` and is stored ORG-OWNED on purpose — `userId` is null so that one `graphs: org-chart: rw` grant covers every deal. That makes ownership the wrong question to ask about it: `findGraph`'s `ownerScope` matches a null owner for any member of the org, so every teammate could read and mutate the buying-committee chart of a deal they were not on, by path, by name or by id. Membership is now asked at RESOLUTION, for the same reason ownership is: all three addressing forms end at that one row, and a check bolted onto a single caller would leave the other two open. `findGraph` runs every resolved row through one gate, which is a no-op for anything that is not a conversation chart. - `GraphScope.canReachConversation` is optional because it is a property of the SURFACE, not of the graph. `graphs.*` supplies it (session user, must be on the deal); the admin router stays deliberately org-wide and the agent tool stays gated by its playbook grant. A scope that omits it is byte-for-byte unchanged. - `graphs.*` answers it with `assertConversationMembership` — the helper `files.*`, `drive.*` and `agent.*` already guard conversation-scoped reads with, so the graph canvas and the file tree beside it agree about who may see a deal. Fails closed on any throw, as `agent.ts` does with it. - `conversationIdFromOrgChartPath` is the inverse of the path builder, and is deliberately exact: a looser match would hand the gate the wrong id for `conversation/{id}/agent-7/notes`, a different document with a different rule. Tested both ways. Also drops a stale duplicated comment above `activeKind` left by an earlier edit — two comments for one constant, the first contradicting the second. Not changed, and worth a look separately: `listGraphs` applies no ownership filter at all, so it still lists other teammates' `user/graphs/*` and every deal's chart. The ids it hands out no longer open anything that check would refuse, so this is an enumeration gap rather than an access one — but it is shared with the agent and admin surfaces, so narrowing it is its own change. Verified: @zero/server and @zero/mail typecheck; 1803 tests pass across services/documents, trpc and the document tools; deps:check clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:50 PMhistoryfix: close the findings from reviewing the fixes themselves A second review pass, this time over the ten fix commits — which were unreviewed code, and which is where the worst finding was. Caps enforced in one place each, missing the path their own docstring cites. The cell-length limit lived only in `applyCellWrites`, so an agent bypassed it by choosing `add_rows` over `set` — those rows go through `materializeRow`, which checked the column name and not the value. Both now call one `assertCellLength`. The column cap lived only in `applyTableOps`, which the import path never reaches: it goes `writeDocument` → `writeFileAsYjs` → `writeTable`, so the "1,000x300 import" the docstring describes was still uncapped. Now checked in `table-import.ts` alongside the row cap. `rename_column` was the one route left to give a column a reserved label — the guard was added to `create` and `add_columns` in the same commit and not there, so the invariant its error message asserts was not actually held. The invalidation hook in `next-step-sync` fires inside `applyUpdate`'s transaction, and it detaches a chain that opens its own connection — so it could read the pre-commit row and cache the stale value back into every bound cell with nothing left to re-fire. Queued as a `postCommitEffect` instead. The five sibling call sites added in the same commit are all on auto-commit connections and were already right. `focusCellAbove`/`focusCellLeft` were added by the keyboard/ARIA commit and never wired to anything — arrow-key cell navigation does not exist, so they were dead code claiming otherwise. Deleted rather than left as a promise. The gutter also needed `role="gridcell"`: `aria-colcount` counts it as column 1 and the data cells start at 2, so without it the row declared a cell that did not exist. And two `_`-prefixed unused bindings in test files I wrote, which the repo forbids outright. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 15, 2026, 2:24 PMmentionfeat: typed cells, live ref chips, and tables as first-class files (design: table-documents phases 6, 7) Phase 6 — a cell chip and a prose chip are literally the same component. ConversationNode and EventNode were split the way FileLinkChip already was (Content + the NodeViewProps wrapper that mounts it), so behaviour stays identical by construction: same data hooks, same click handler, same setSelectedArtifact path. The only PM-specific parts were NodeViewWrapper and reading node.attrs, which is why the extraction is small. Editing mounts a one-line TipTap instance for the focused cell ONLY, configured with the EXISTING suggestion extensions — so typing `@` in a table cell runs the same extension, the same search, and inserts the same node as typing `@` in a prose document, rather than a parallel implementation that drifts. Exactly one editor is alive at a time, so the descriptor-tree costs that rule out a ProseMirror grid never apply here. Phase 7 — "New table" in the file-creation menu, a table graph-node kind, "N rows × M columns" from the metadata the server hook already writes, the grid in the artifact panel (discriminated inside FileArtifact on the FETCHED documentType, since the outer switch is on ContextKind where a table is just a `file`), a read-only grid on the public share page, resolveOpenDoc, and Download as Excel/CSV wired to documents.exportTable. Also completes Phase 10's agent-facing half: the `table` tool's `create` now takes a `from` clause, so an agent materializes rows from a real query instead of transcribing them. The query runs BEFORE the document is provisioned, so an over-cap or malformed filter leaves no half-built table behind, and truncation plus any binding error is surfaced as a tool warning rather than silently yielding a short or empty column. Three fixes found by running the suites rather than by reading: - The tool description had grown to 2134 chars, past the 2048 limit Claude Code truncates at — text past the cutoff is silently dropped for every caller. Trimmed to 2030 while ADDING the `from` guidance, which is the part most worth having early. - tableTool.test.ts enumerated its mock of the documents barrel, so every new import in the tool broke the file with a mock error rather than a real failure. It now spreads the real module. - A cell-ref offset expectation was hand-counted one short (`[[conversation: 9f2e]]` is 22 chars, so end is 27). The implementation was right; the test now asserts by slicing the value, since these offsets exist precisely so the grid can interleave text and chips. Tests: 1787 server, 55 mail across the table/home/files suites. Both apps typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 10, 2026, 5:04 PMplaybookMerge pull request #2766 from CedarCopilot/fix/chat-thread-cross-user-idor fix(auth): close chat-thread cross-user IDOR, restore admin scope on Brain playbook navSep 12, 2026, 11:54 AMpropertiesfeat(graph): a node is a person — face, name, title, link The panel was drawing a person as a list of key/value rows, including the rows that ARE the person: `Title Director of Sales` under a heading that said their name, `LinkedIn https://…` as raw text, and `Person <email>` — plumbing — as a property. That is the same information the contact panel beside a conversation already renders as a profile, and there is no reason a reader should meet two renderings of one human. So a node now draws as a profile header: avatar, name, LinkedIn mark, title, company — the shape `PersonProfile` uses, in the inspector at `lg` and on the canvas card at `sm`. The headline sits under it. A person nobody has enriched is a circle with two initials, which is exactly as much as is known. **It is keyed off `from`, not off field keys.** A field bound to `person.photoUrl` IS the avatar, whatever it is called. `from` is a closed catalog vocabulary the server already validates; a key is free text an agent minted, so keying on `photo` would mean a graph that called it `avatar` drew no face with nothing on screen to say why. It also means no new schema vocabulary: a graph gets a header by binding fields it would have bound anyway, and a graph of COMPANIES binds none of them and draws as before. Whatever the header drew is then SUBTRACTED from the rows — including from the undeclared-key rule, via `hiddenKeys`, because the node's bag really does still carry `person` and it would otherwise reappear directly under the header that replaced it. **The org chart declares `photo` and `headline`**, bound to `person.photoUrl` and `person.headline`. **And the agent can finally fill them.** `graphEnrich` has existed in the service and on both tRPC routes since phase 7, but `GRAPH_ACTIONS` never listed it — so the one agent whose whole job is this chart could not enrich anybody on it. `graph.enrich` is now a verb, behind a WRITE grant even for the free report: the report and the spend are one act from the agent's point of view, and a grant that let it price something it could not buy would only teach it to try. The prompt tells it to use **fiber**: $0.04 a person against crustdata's $0.10, and the only provider that returns `person.photoUrl` and `person.headline` — the two fields the faces are made of. Two calls, the first free, always with a `limit`, and skip the second when the report says everyone is already enriched. Co-Authored-By: Claude Opus 5 <<email>>Sep 20, 2026, 11:15 PMrecordingfix(meetings): a Circleback recording that plays, on open, with a link that goes to Circleback Three things stood between a Circleback meeting and its recording. The recording could not be fetched at all. credentials-encryption derives its AES key from ORG_CREDENTIALS_ENCRYPTION_KEY || BETTER_AUTH_SECRET, so which secret a credential was written under is a property of the environment at WRITE time. Every Circleback MCP token set was written under BETTER_AUTH_SECRET; once the org key was set, every decrypt died with "Cipher job failed", the call sites swallowed it into "no credentials", and the player said "No playable recording for this meeting" with the real cause visible nowhere. Decryption now tries each configured secret; encryption still writes with the first. The meeting dialog waited for a press before fetching. That poster exists so a coaching document with ten moments does not fire ten mints on open against a Fathom budget of 30/min — an argument that does not apply to one recording the reader opened to watch. `autoLoad` skips the press without autoplaying. "Open in Circleback" went to storage.googleapis.com. What ingest stored is a signed GCS object whose signature dies 24h later; unsigned it answers 403 forever. The page is built from the meeting id instead, and no surface will link to a raw media file any more. Webhook-era rows (numeric external_id, all before 2026-07-29) still cannot mint or link — neither ReadMeetings nor the page route takes a numeric id. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 5, 2026, 12:21 PMtablefeat: the transcript names what it did, and the table it made is reachable A chat that spent twenty calls building a LinkedIn tracker recorded almost none of it: four different people got connection requests under four identical "Executing linkedin-write" lines, the table itself never became a context item, and re-entering the chat reopened the deal instead of the table. Traced end to end; this is the set of causes. The transcript: - Tool labels resolve from the ARGUMENTS, not just the name. Every tool is an action family now, so the name alone collapses a dozen verbs into one row. Rows expand to who / where / whether a message went with it. - A select column's declared option order IS its sort ladder; the tool description now says so, and the transcript shows the ladder. LinkedIn caps: - A send's provider-id lookup gets its own budget (connect/dm/inmail_lookup), mirroring the send it serves. Charged to profile_view, a day of browsing refused invitations that had 80/week of connect headroom left. - Connection requests go out with NO note unless the user asked for one — enforced at execute time, not just described. Getting back to what the agent made: - The table tool files a context chip on create/import, as write-document has always done; delete pulls it back off. - A thread remembers what it was displaying (chat_threads.context), so re-entry restores the table rather than re-deriving the deal. - A fan-out opens a conversation-scoped table inside its deal's Files tab. - 'table' is a valid write type on the document family — the tool was telling the agent to pass a value its own schema rejected, which is what drove a delete-and-rebuild that orphaned the open document. The grid: - Title, then description, then the commands — the order the questions arrive in; a URL or address in a cell is a real link; a deleted table says so instead of rendering on from the local Y.Doc cache. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:43 PMyjsMerge remote-tracking branch 'origin/staging' into refactor/agent-files-sections # Conflicts: # apps/mail/modules/agents/components/AgentConversationFilesFolder.tsx # apps/mail/modules/conversations/components/files/ConversationFileTree.tsx # apps/mail/modules/files/components/list/FileListColumns.tsx # apps/mail/modules/files/components/list/index.ts # apps/mail/modules/files/components/list/types.ts # apps/mail/tests/modules/files/brain-explorer.test.tsx # apps/mail/tests/modules/files/list/FileListRow.test.tsx # apps/server/src/mastra/tools/debug/debugTool.ts # apps/server/src/services/playbook/playbook-execution-triggers.ts # apps/server/src/services/playbook/playbook-renderers.ts # apps/server/src/services/playbook/resolve-playbook-context.ts # apps/server/src/services/playbook/write-document-guarded.ts # apps/server/src/trpc/routes/documents.tsSep 9, 2026, 9:26 AMattachment-file-view.tsxfeat(files): every file surface renders one row (design: unified-files-ui phases 3-6) /brain, a conversation's Files tab and an agent's Output/Memory tabs were ten row renderers that had each re-decided indentation, icon chrome, type size, selected colour and separators. The same file looked like a different object depending on where you found it. They now all render FileListRow. Shape is Google Drive's, reworked from review: - the column header IS the top row; no count strip above it - the right edge is one fixed cell holding a ⋮ menu, so a row with four affordances and a row with one still end at the same x - no "Edited by" column — the actor rides inside Modified ("3 days ago by Jesse Li", "by you" for the viewer). The two read as one fact, and splitting them spent a column's width to say something the reader reassembles anyway - a folder's count sits beside its title, "Archives (50)", not as a lone number at the far right where it reads as a column only some rows have - "New file"/"New folder" live in the header's + and in each folder's own ⋮, never a strip above the list that pushes the files down the page - a scope root is a ROW, not a section header. It is a folder — you open it, drop into it, create inside it — so it gets the folder's row, just a prominent one Selection and keyboard, matching the thread list where it exists and diverging only where the divergence is the point: - hovering cross-fades the tile into a square checkbox in the same footprint; pure CSS, because a tree re-rendering every row on mouse-move is unusable - hover reaches the keyboard by CustomEvent into a ref, never state - keys act on hover FIRST, then selection — the cursor is the more specific statement of intent - the letters are the task list's (w delete, x select, ⇧X range), but w CONFIRMS here: mail can delete instantly because its undo stack reverses a label change, and a folder takes its whole subtree with it Two bugs the migration surfaced, both from Radix restoring focus to a menu trigger a tick after close: a popover opened from a ⋮ item dismissed itself, and Rename's autofocused input blurred and cancelled in the same frame. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 11:21 PMchip-styles.tsfeat: typed cells, live ref chips, and tables as first-class files (design: table-documents phases 6, 7) Phase 6 — a cell chip and a prose chip are literally the same component. ConversationNode and EventNode were split the way FileLinkChip already was (Content + the NodeViewProps wrapper that mounts it), so behaviour stays identical by construction: same data hooks, same click handler, same setSelectedArtifact path. The only PM-specific parts were NodeViewWrapper and reading node.attrs, which is why the extraction is small. Editing mounts a one-line TipTap instance for the focused cell ONLY, configured with the EXISTING suggestion extensions — so typing `@` in a table cell runs the same extension, the same search, and inserts the same node as typing `@` in a prose document, rather than a parallel implementation that drifts. Exactly one editor is alive at a time, so the descriptor-tree costs that rule out a ProseMirror grid never apply here. Phase 7 — "New table" in the file-creation menu, a table graph-node kind, "N rows × M columns" from the metadata the server hook already writes, the grid in the artifact panel (discriminated inside FileArtifact on the FETCHED documentType, since the outer switch is on ContextKind where a table is just a `file`), a read-only grid on the public share page, resolveOpenDoc, and Download as Excel/CSV wired to documents.exportTable. Also completes Phase 10's agent-facing half: the `table` tool's `create` now takes a `from` clause, so an agent materializes rows from a real query instead of transcribing them. The query runs BEFORE the document is provisioned, so an over-cap or malformed filter leaves no half-built table behind, and truncation plus any binding error is surfaced as a tool warning rather than silently yielding a short or empty column. Three fixes found by running the suites rather than by reading: - The tool description had grown to 2134 chars, past the 2048 limit Claude Code truncates at — text past the cutoff is silently dropped for every caller. Trimmed to 2030 while ADDING the `from` guidance, which is the part most worth having early. - tableTool.test.ts enumerated its mock of the documents barrel, so every new import in the tool broke the file with a mock error rather than a real failure. It now spreads the real module. - A cell-ref offset expectation was hand-counted one short (`[[conversation: 9f2e]]` is 22 chars, so end is 27). The implementation was right; the test now asserts by slicing the value, since these offsets exist precisely so the grid can interleave text and chips. Tests: 1787 server, 55 mail across the table/home/files suites. Both apps typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 10, 2026, 5:04 PMdeferred-auto-convert.tsfix: playbook trigger/stage parsing and CRM updater doc editor Preserve event trigger if: clauses on edit, stop splitting kebab-case stage ids on hyphens, and surface the crm-updater subagent doc in the CRM settings tab. Co-Authored-By: Claude Sonnet 4.6 (1M context) <<email>> Co-authored-by: Cursor <<email>>Jun 16, 2026, 1:07 AMdocument.tsxfix(agents): thread targetUserId into the instructions doc editor + doc-events SSE Karim (Adapt Insurance, org admin) reported Jacob's agent instructions not showing when viewing his agent — the admin-view targetUserId prop reached every trpc.agent.* call but never the shared <Document> Y.js editor that AgentInstructionsSection hands the actual instructions content to. Document had no targetUserId concept at all, so its documents.getDoc fetch ran under the admin's own session; the server correctly rejected it as a cross-user read (row.userId !== userId on a user/-scoped doc), and the failure was swallowed into a console.error with no onProviderError handler wired up — the editor just rendered a permanently empty Y.Doc. - Document/DocumentProps gains targetUserId, forwarded to both the initial getDoc seed and refreshFromServer. - AgentInstructionsSection forwards its own targetUserId prop down into <Document> instead of dropping it at the door. - /api/doc-events (the live-update SSE channel) had the same shape of gap: a user/-scoped doc's live stream only allowed the doc's own owner or Cedar staff, so even a fixed initial read would 403 on remote updates. Routed through assertActorAllowed — the same authority documents.getDoc already uses — instead of a flat isCedarStaff check. files.applyUpdate (the write path) needed no change: it already derives the document's scope from the row itself via assertAuthorizedScope, not from a client-supplied id, so org-admin writes were already authorized correctly. Co-Authored-By: Claude Sonnet 5 <<email>> Claude-Session: https://claude.ai/code/session_01SfYKPYHXdMgrPLuKhXvWedSep 11, 2026, 12:29 PMDocumentBackGutter.tsxfeat(tasks): a CRM updates lane, meeting-aware routing, and narrow-panel polish field-approval was the single largest Misc population (374 of a 677-task backlog) with nowhere to go — work that finishes inside the CRM rather than in someone's inbox now has its own default lane. The router additionally counts meetings already held on a deal, which is what makes "first meeting" vs "subsequent meeting" lanes decidable; countMeetingsSoFar is shared with the opt-in post-meeting split script so a task lands in the same place whichever path filed it. Backfill window/limit/confidence are env-overridable because a newly-added lane leaves a deeper backlog than the routine 100/user. Also in this commit (whole-tree — the session had no edit history to scope by): - Back gutters collapsed a breakpoint too early; ~760px/75ch columns keep usable margin past @max-4xl, so the swap moved to @max-3xl (wide-column hosts 5xl -> 4xl) across threads, conversations, playbooks and Brain. - Calendar event popover: larger title, single-line date row, and locations and descriptions run through LinkifiedEventText so meeting links are clickable. - Snippets shrinks to an icon-only Braces button; the inbox shortcut legend drops the channel switch and folds range/multi-select into one "s" hint. - AOP custom-field column building extracted to crm/utils/aop-columns, and InboxLinkedDealsSection split out (its test already imported the path). Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 4, 2026, 7:45 PMfull-bleed-doc-types.tsfeat: the org-chart agent, and the graph wiki (design: graph-documents phase 7) An org chart exists with NO org-chart-specific code anywhere in the repo. It is a graph whose schema happens to describe people and whose view happens to rank by reporting — and `graph-templates.test.ts` enforces that by asserting every template is expressible entirely in `nodeSchema` + `edgeKinds` + `view`. The subagent is instructed to create from the template rather than hand-author a schema, point each node at the person's EXISTING contact profile, draw an edge only with named evidence recorded in the edge's `meta`, call `field_values` before writing a value so it does not invent a fifth stance, read its lint delta after every write — and NEVER delete a node. Someone who has gone quiet gets `stance: unknown`, because going quiet is itself the signal. Two things worth knowing: - `org-chart-seed.test.ts` extracts every `graph.<action>` the prompt names and runs each through the tool's OWN input schema, plus a negative case so the check is not vacuous. It is the only thing that catches the prompt drifting from the tool, and it fails in CI rather than at 3am. - The document and the `<ref>` ship together. A subagent only runs when the playbook references it, so seeding the instructions alone would ship an agent that never runs. It takes a cron block of its own rather than joining the 07:00 chain — a long graph pass in front of the morning agenda buys it nothing. Also: `docs/wiki/graph-documents.md`, covering what this run learned the hard way — why nodes live inside the Y.Doc when a board's cards live outside, why the lossy mirror is what forced `writeGraphAsYjs` into existence, the key-order bug that would have churned `content_yjs` on every save while every deep-equality test stayed green, and the fact that the frontend has FIVE document-type dispatch sites rather than a registry. That last one is also indexed inline, in `full-bleed-doc-types.ts`, which is the closest thing to the registry it is not. 1,887 passing across playbook, aop, graph and tools. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 10:09 PMhtml-document-view.tsxfeat(sharing): typed links, and the count you can check (design: sharing-ui-actions phase 4) The anonymous eternal token is retired from the UI. `html-share-dialog.tsx` — 188 lines, four `toast.error` sites, a Dialog that dimmed the very document it was describing — is deleted, and its call site mounts the real control. A link now names its audience, expires, and can carry a password. Defaults are the narrowest useful thing rather than the widest: Anyone at Cedar · Can view · 30 days. `manager` is absent by construction — only manage may re-share, and a bearer credential that confers the right to hand out more of itself is a credential with no floor. `editor` IS offered, because the role is now enforced: `accessFor` folds a redeemed link's standing through the same `max` as a grant. Copy works with sharing OFF, which is the case the old dialog could not express at all and the common one: most of the time you are sending a colleague a pointer, not publishing. It copies the in-app URL and says "Only people with access can open this" underneath, so the reader knows which of the two they just did. Revoking states the count first — "opened 34 times. Anyone using it loses access immediately" — which is what `viewCount` was added for. A live link's settings are deliberately NOT editable. The URL is already out there: editing in place either silently changes what a sent link does, or rotates the token and breaks it invisibly. Settings are chosen in a composer before the mint, and that Create is the one Save-shaped button in the panel — R1 is about ambient state, and a half-configured bearer credential must not exist between two clicks. And the private-folder confirm gains its disclosure. The count had no way to be checked; now the same server walk that produced the number returns the names behind it, so "9 people lose access" is a fact somebody can open and read. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 4:11 PMuse-active-doc-context.tsfeat(chat): the agent you are in, and the file you are reading, are context Opening a file from a file browser — the agent workspace's Output tab, a deal's Files tab, the Brain explorer — published nothing at all, and an agent workspace published nothing anywhere. So a question asked while looking at both arrived with neither: "this file" and "this agent" named things the chat had never been told about. Both now ride the wire as badges beside the others, dismissable the same way, and the file carries its documentId — which is what makes the ambient file attachable at all, since manage-context takes an id and a path is not one. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 9, 2026, 1:07 PMuse-rich-text-extensions.tschore: snapshot in-progress working tree before layout-slice refactor Pre-existing WIP across chat input, file-link mentions, nav buttons, agenda, and server file-system, committed to clear the runway for the unified layout slice implementation (design: layout-slice). Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 11, 2026, 8:20 PM