Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4516c29
feat(content): preserve database block identity
3mdistal Aug 10, 2026
da1846f
feat(content): make row mutations reliable
3mdistal Aug 10, 2026
616e673
Merge commit 'da1846f55feea1316da3793735482e477679ec25' into codex/co…
3mdistal Aug 10, 2026
6d1779b
Merge commit '4516c29143db503ee9af96ced7b1a03cd682f5cf' into codex/co…
3mdistal Aug 10, 2026
fd4aa9e
fix: repair Content row mutation CI coverage
3mdistal Aug 10, 2026
04d0004
feat(content): add exact database block actions
3mdistal Aug 10, 2026
fc634ac
fix(content): keep block identity portable
3mdistal Aug 10, 2026
aa4b3cb
fix(content): make block migration retry-safe
3mdistal Aug 10, 2026
6b764e3
fix(content): harden exact block mutations
3mdistal Aug 10, 2026
dbce466
fix(content): preserve nested block tombstones
3mdistal Aug 10, 2026
0778954
Merge remote-tracking branch 'origin/pr-2779-current' into codex/cont…
3mdistal Aug 11, 2026
7b03c15
Merge remote-tracking branch 'origin/pr-2778-current' into codex/cont…
3mdistal Aug 11, 2026
95cc102
Merge branch 'codex/content-feedback-block-actions-base' into codex/c…
3mdistal Aug 11, 2026
b9443b9
fix(content): protect additional block field writes
3mdistal Aug 11, 2026
7d992b6
test(content): cover absent block field races
3mdistal Aug 11, 2026
91346b4
Merge current main into Content block actions
3mdistal Aug 14, 2026
f7a936e
test(content): initialize block action postgres fixture
3mdistal Aug 14, 2026
19b48cf
fix(content): expose block mutations to agents
3mdistal Aug 14, 2026
c98298e
fix: reject raced block id collisions
3mdistal Aug 14, 2026
ad56fd2
fix(content): close block mutation review gaps
3mdistal Aug 14, 2026
d84cec2
Merge remote-tracking branch 'origin/main' into codex/content-feedbac…
3mdistal Aug 14, 2026
1d1519b
fix(content): regenerate merged parity matrix
3mdistal Aug 14, 2026
ed6faff
fix(content): honor existing upsert position
3mdistal Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ its stored name). In table views a Blocks column shows a word count (e.g.
database view's column menu (not from the page body); deleting the last
Blocks field warns that it removes the body for every object of the type.

For one-block agent edits, call `list-content-database-blocks` with the exact
space, database, backing document, membership row, row document, and property
IDs. Preserve its schema, row, and field revisions. Each returned block names
the operations its kind supports and carries canonical Notion-flavored Markdown
(NFM) for that one block. Pass all three revisions to
`mutate-content-database-block`; its `insert`, `update`, `upsert`, `delete`, and
`reorder` variants preserve unmentioned fields and sibling blocks. A block
value must contain exactly one top-level block of the declared kind. Reuse an
idempotency key only for an exact retry. Unsupported kinds, kind conversion,
tombstone reuse, cross-parent reorder, schema drift, and stale row or field
revisions fail explicitly.

Formula properties store their expression in property options and support
`{Property name}` substitution plus simple numeric math such as `{MSV} * 2`.

Expand Down Expand Up @@ -236,6 +248,7 @@ Use `create-content-database`, `create-inline-content-database`,
`duplicate-database-items`, `remove-database-items`, `move-database-item`,
`update-content-database-view`, `list-document-properties`,
`configure-document-property`, `set-document-property`,
`list-content-database-blocks`, `mutate-content-database-block`,
`duplicate-document-property`, and `delete-document-property`; do not edit
property rows or view config via raw SQL when an action can do it.

Expand Down
2 changes: 2 additions & 0 deletions templates/content/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ Read the relevant skill before deeper work:
| `edit-document` | Find/replace edit — preferred for small changes |
| `update-document` | Full rewrite of title, content, or description |
| `delete-document` | Move a page and its children to Trash |
| `list-content-database-blocks` | List stable blocks and revisions in one exact database row/property |
| `mutate-content-database-block` | Insert, update, upsert, delete, or reorder one supported stable block |
| `migrate-content-database-rows` | Validate, atomically apply, verify, roll back, or finalize one bounded whole-database row migration |

Every action carries its own schema, and the rest of the app-specific surface
Expand Down
17 changes: 17 additions & 0 deletions templates/content/actions/_blocks-field-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ export class BlocksFieldRevisionConflictError extends Error {
}
}

export class BlocksFieldIdCollisionError extends Error {
readonly blockId: string;
readonly statusCode = 409;

constructor(blockId: string) {
super(`Block ID is already owned by another Blocks field: ${blockId}`);
this.name = "BlocksFieldIdCollisionError";
this.blockId = blockId;
}
}

function nanoid(size = 12): string {
const chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
Expand Down Expand Up @@ -265,6 +276,8 @@ export async function persistBlocksFieldIdentity(args: {
previousMarkdown: string;
markdown: string;
expectedRevision?: number;
preferredIdsByPath?: Readonly<Record<string, string>>;
rejectCrossFieldIdRemapping?: boolean;
now: string;
}): Promise<StoredBlocksFieldIdentity> {
const fieldId = blocksFieldId(args.documentId, args.propertyId);
Expand Down Expand Up @@ -306,6 +319,7 @@ export async function persistBlocksFieldIdentity(args: {
previous,
markdown: args.markdown,
createId: () => `block_${nanoid(16)}`,
preferredIdsByPath: args.preferredIdsByPath,
});

if (next.blocks.length > 0) {
Expand All @@ -325,6 +339,9 @@ export async function persistBlocksFieldIdentity(args: {
const reserved = new Set(next.blocks.map((block) => block.id));
for (const existing of existingOwners) {
if (existing.fieldId === fieldId) continue;
if (args.rejectCrossFieldIdRemapping) {
throw new BlocksFieldIdCollisionError(existing.id);
}
let replacement = `block_${nanoid(16)}`;
while (reserved.has(replacement)) replacement = `block_${nanoid(16)}`;
reserved.add(replacement);
Expand Down
Loading
Loading