@agent-native/core 0.154.3 → 0.154.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
- package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
- package/corpus/templates/content/actions/_database-utils.ts +55 -3
- package/corpus/templates/content/actions/_property-utils.ts +141 -31
- package/corpus/templates/content/actions/add-database-item.ts +62 -240
- package/corpus/templates/content/actions/configure-document-property.ts +170 -1
- package/corpus/templates/content/actions/delete-document-property.ts +56 -17
- package/corpus/templates/content/actions/delete-document.ts +3 -0
- package/corpus/templates/content/actions/edit-document.ts +18 -1
- package/corpus/templates/content/actions/export-document.ts +41 -0
- package/corpus/templates/content/actions/remove-database-items.ts +10 -0
- package/corpus/templates/content/actions/set-document-property.ts +140 -3
- package/corpus/templates/content/actions/update-database-item.ts +62 -0
- package/corpus/templates/content/actions/update-document.ts +24 -0
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
- package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
- package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
- package/corpus/templates/content/app/i18n-data.ts +20 -0
- package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
- package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
- package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
- package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/server/db/schema.ts +100 -3
- package/corpus/templates/content/server/plugins/db.ts +85 -0
- package/corpus/templates/content/shared/api.ts +78 -2
- package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
- package/corpus/templates/content/shared/document-export.ts +33 -1
- package/dist/action.d.ts +22 -0
- package/dist/action.js +27 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +7 -2
- package/dist/extensions/url-safety.js +27 -12
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-client/oauth-client.d.ts +16 -1
- package/dist/mcp-client/oauth-client.js +177 -83
- package/dist/mcp-client/remote-store.js +46 -20
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/index.d.ts +2 -1
- package/dist/oauth-tokens/index.js +2 -1
- package/dist/oauth-tokens/lifecycle.d.ts +87 -0
- package/dist/oauth-tokens/lifecycle.js +434 -0
- package/dist/oauth-tokens/migrations.js +10 -0
- package/dist/oauth-tokens/store.d.ts +22 -0
- package/dist/oauth-tokens/store.js +141 -18
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.js +10 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { docToNfm, nfmToDoc, type PMNode } from "./nfm.js";
|
|
2
|
+
|
|
3
|
+
export const BLOCKS_FIELD_IDENTITY_VERSION = 1;
|
|
4
|
+
export const BLOCK_TOMBSTONE_REVISION_WINDOW = 20;
|
|
5
|
+
export const MAX_BLOCK_TOMBSTONES_PER_FIELD = 500;
|
|
6
|
+
|
|
7
|
+
export type BlocksFieldIdentityStatus = "legacy" | "materialized" | "stale";
|
|
8
|
+
|
|
9
|
+
export interface BlocksFieldBlock {
|
|
10
|
+
id: string;
|
|
11
|
+
parentId: string | null;
|
|
12
|
+
kind: string;
|
|
13
|
+
position: number;
|
|
14
|
+
addressable: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BlocksFieldTombstone {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: string;
|
|
20
|
+
deletedAtRevision: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BlocksFieldIdentity {
|
|
24
|
+
version: typeof BLOCKS_FIELD_IDENTITY_VERSION;
|
|
25
|
+
fieldId: string;
|
|
26
|
+
revision: number;
|
|
27
|
+
contentHash: string;
|
|
28
|
+
identityStatus: BlocksFieldIdentityStatus;
|
|
29
|
+
blocks: BlocksFieldBlock[];
|
|
30
|
+
tombstones: BlocksFieldTombstone[];
|
|
31
|
+
recoveryMode: "editor-undo-with-tombstones";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StoredBlocksFieldBlock extends BlocksFieldBlock {
|
|
35
|
+
contentHash: string;
|
|
36
|
+
markdown: string;
|
|
37
|
+
deletedAtRevision: number | null;
|
|
38
|
+
recoveredAtRevision: number | null;
|
|
39
|
+
state: "live" | "deleted";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StoredBlocksFieldIdentity {
|
|
43
|
+
fieldId: string;
|
|
44
|
+
revision: number;
|
|
45
|
+
contentHash: string;
|
|
46
|
+
blocks: StoredBlocksFieldBlock[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BlockSnapshot {
|
|
50
|
+
path: string;
|
|
51
|
+
parentPath: string | null;
|
|
52
|
+
kind: string;
|
|
53
|
+
position: number;
|
|
54
|
+
addressable: boolean;
|
|
55
|
+
contentHash: string;
|
|
56
|
+
markdown: string;
|
|
57
|
+
preferredId: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BLOCK_NODE_TYPES = new Set([
|
|
61
|
+
"paragraph",
|
|
62
|
+
"heading",
|
|
63
|
+
"horizontalRule",
|
|
64
|
+
"codeBlock",
|
|
65
|
+
"blockquote",
|
|
66
|
+
"bulletList",
|
|
67
|
+
"orderedList",
|
|
68
|
+
"listItem",
|
|
69
|
+
"taskList",
|
|
70
|
+
"taskItem",
|
|
71
|
+
"notionToggle",
|
|
72
|
+
"notionCallout",
|
|
73
|
+
"notionColumns",
|
|
74
|
+
"notionColumn",
|
|
75
|
+
"notionSyncedBlock",
|
|
76
|
+
"table",
|
|
77
|
+
"tableRow",
|
|
78
|
+
"tableHeader",
|
|
79
|
+
"tableCell",
|
|
80
|
+
"image",
|
|
81
|
+
"video",
|
|
82
|
+
"audio",
|
|
83
|
+
"notionBlockAtom",
|
|
84
|
+
"registryBlock",
|
|
85
|
+
"contentReference",
|
|
86
|
+
"localMdxComponent",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const NON_ADDRESSABLE_NODE_TYPES = new Set([
|
|
90
|
+
"bulletList",
|
|
91
|
+
"orderedList",
|
|
92
|
+
"taskList",
|
|
93
|
+
"tableRow",
|
|
94
|
+
"tableHeader",
|
|
95
|
+
"tableCell",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
function hashString(value: string): string {
|
|
99
|
+
let first = 0x811c9dc5;
|
|
100
|
+
let second = 0x9e3779b9;
|
|
101
|
+
for (let index = 0; index < value.length; index++) {
|
|
102
|
+
const code = value.charCodeAt(index);
|
|
103
|
+
first ^= code;
|
|
104
|
+
first = Math.imul(first, 0x01000193);
|
|
105
|
+
second ^= code + index;
|
|
106
|
+
second = Math.imul(second, 0x85ebca6b);
|
|
107
|
+
}
|
|
108
|
+
return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0)
|
|
109
|
+
.toString(16)
|
|
110
|
+
.padStart(8, "0")}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function blocksFieldId(documentId: string, propertyId: string): string {
|
|
114
|
+
return `blocks_field_${hashString(`${documentId}\0${propertyId}`)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function blocksContentHash(markdown: string): string {
|
|
118
|
+
return `nfm_${hashString(markdown)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stableNodeValue(value: unknown): unknown {
|
|
122
|
+
if (Array.isArray(value)) return value.map(stableNodeValue);
|
|
123
|
+
if (!value || typeof value !== "object") return value;
|
|
124
|
+
return Object.fromEntries(
|
|
125
|
+
Object.entries(value as Record<string, unknown>)
|
|
126
|
+
.filter(([key]) => key !== "blockId")
|
|
127
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
128
|
+
.map(([key, child]) => [key, stableNodeValue(child)]),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function nodeMarkdown(node: PMNode): string {
|
|
133
|
+
return docToNfm({ type: "doc", content: [node] });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function snapshotMarkdown(markdown: string): BlockSnapshot[] {
|
|
137
|
+
const snapshots: BlockSnapshot[] = [];
|
|
138
|
+
const doc = nfmToDoc(markdown);
|
|
139
|
+
|
|
140
|
+
function visit(nodes: PMNode[] | undefined, parentPath: string | null) {
|
|
141
|
+
let blockPosition = 0;
|
|
142
|
+
for (let index = 0; index < (nodes?.length ?? 0); index++) {
|
|
143
|
+
const node = nodes![index]!;
|
|
144
|
+
if (!BLOCK_NODE_TYPES.has(node.type)) continue;
|
|
145
|
+
const path =
|
|
146
|
+
parentPath === null
|
|
147
|
+
? `${blockPosition}`
|
|
148
|
+
: `${parentPath}.${blockPosition}`;
|
|
149
|
+
const canonical = JSON.stringify(stableNodeValue(node));
|
|
150
|
+
snapshots.push({
|
|
151
|
+
path,
|
|
152
|
+
parentPath,
|
|
153
|
+
kind: node.type,
|
|
154
|
+
position: blockPosition,
|
|
155
|
+
addressable: !NON_ADDRESSABLE_NODE_TYPES.has(node.type),
|
|
156
|
+
contentHash: `block_${hashString(canonical)}`,
|
|
157
|
+
markdown: nodeMarkdown(node),
|
|
158
|
+
preferredId:
|
|
159
|
+
node.type === "registryBlock" &&
|
|
160
|
+
typeof node.attrs?.blockId === "string"
|
|
161
|
+
? node.attrs.blockId
|
|
162
|
+
: null,
|
|
163
|
+
});
|
|
164
|
+
visit(node.content, path);
|
|
165
|
+
blockPosition++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
visit(doc.content, null);
|
|
170
|
+
return snapshots;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function deterministicBlockId(
|
|
174
|
+
fieldId: string,
|
|
175
|
+
snapshot: Pick<BlockSnapshot, "path" | "kind" | "contentHash">,
|
|
176
|
+
): string {
|
|
177
|
+
return `block_${hashString(
|
|
178
|
+
`${fieldId}\0${snapshot.path}\0${snapshot.kind}\0${snapshot.contentHash}`,
|
|
179
|
+
)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fieldScopedBlockId(fieldId: string, candidateId: string): string {
|
|
183
|
+
return `block_${hashString(fieldId)}_${hashString(candidateId)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function publicIdentity(
|
|
187
|
+
stored: StoredBlocksFieldIdentity,
|
|
188
|
+
identityStatus: BlocksFieldIdentityStatus,
|
|
189
|
+
): BlocksFieldIdentity {
|
|
190
|
+
return {
|
|
191
|
+
version: BLOCKS_FIELD_IDENTITY_VERSION,
|
|
192
|
+
fieldId: stored.fieldId,
|
|
193
|
+
revision: stored.revision,
|
|
194
|
+
contentHash: stored.contentHash,
|
|
195
|
+
identityStatus,
|
|
196
|
+
blocks: stored.blocks
|
|
197
|
+
.filter((block) => block.state === "live")
|
|
198
|
+
.map(({ id, parentId, kind, position, addressable }) => ({
|
|
199
|
+
id,
|
|
200
|
+
parentId,
|
|
201
|
+
kind,
|
|
202
|
+
position,
|
|
203
|
+
addressable,
|
|
204
|
+
})),
|
|
205
|
+
tombstones: stored.blocks
|
|
206
|
+
.filter(
|
|
207
|
+
(
|
|
208
|
+
block,
|
|
209
|
+
): block is StoredBlocksFieldBlock & {
|
|
210
|
+
deletedAtRevision: number;
|
|
211
|
+
} => block.state === "deleted" && block.deletedAtRevision !== null,
|
|
212
|
+
)
|
|
213
|
+
.map(({ id, kind, deletedAtRevision }) => ({
|
|
214
|
+
id,
|
|
215
|
+
kind,
|
|
216
|
+
deletedAtRevision,
|
|
217
|
+
})),
|
|
218
|
+
recoveryMode: "editor-undo-with-tombstones",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function legacyBlocksFieldIdentity(args: {
|
|
223
|
+
documentId: string;
|
|
224
|
+
propertyId: string;
|
|
225
|
+
markdown: string;
|
|
226
|
+
}): BlocksFieldIdentity {
|
|
227
|
+
const fieldId = blocksFieldId(args.documentId, args.propertyId);
|
|
228
|
+
const snapshots = snapshotMarkdown(args.markdown);
|
|
229
|
+
const idByPath = new Map<string, string>();
|
|
230
|
+
const usedIds = new Set<string>();
|
|
231
|
+
const blocks: StoredBlocksFieldBlock[] = snapshots.map((snapshot) => {
|
|
232
|
+
const preferred = snapshot.preferredId
|
|
233
|
+
? fieldScopedBlockId(fieldId, snapshot.preferredId)
|
|
234
|
+
: null;
|
|
235
|
+
const id =
|
|
236
|
+
preferred && !usedIds.has(preferred)
|
|
237
|
+
? preferred
|
|
238
|
+
: deterministicBlockId(fieldId, snapshot);
|
|
239
|
+
usedIds.add(id);
|
|
240
|
+
idByPath.set(snapshot.path, id);
|
|
241
|
+
return {
|
|
242
|
+
id,
|
|
243
|
+
parentId: snapshot.parentPath
|
|
244
|
+
? (idByPath.get(snapshot.parentPath) ?? null)
|
|
245
|
+
: null,
|
|
246
|
+
kind: snapshot.kind,
|
|
247
|
+
position: snapshot.position,
|
|
248
|
+
addressable: snapshot.addressable,
|
|
249
|
+
contentHash: snapshot.contentHash,
|
|
250
|
+
markdown: snapshot.markdown,
|
|
251
|
+
state: "live",
|
|
252
|
+
deletedAtRevision: null,
|
|
253
|
+
recoveredAtRevision: null,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
return publicIdentity(
|
|
257
|
+
{
|
|
258
|
+
fieldId,
|
|
259
|
+
revision: 0,
|
|
260
|
+
contentHash: blocksContentHash(args.markdown),
|
|
261
|
+
blocks,
|
|
262
|
+
},
|
|
263
|
+
"legacy",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function uniqueIndexByKey<T>(
|
|
268
|
+
values: T[],
|
|
269
|
+
key: (value: T) => string,
|
|
270
|
+
): Map<string, number> {
|
|
271
|
+
const counts = new Map<string, number>();
|
|
272
|
+
const index = new Map<string, number>();
|
|
273
|
+
values.forEach((value, valueIndex) => {
|
|
274
|
+
const valueKey = key(value);
|
|
275
|
+
counts.set(valueKey, (counts.get(valueKey) ?? 0) + 1);
|
|
276
|
+
index.set(valueKey, valueIndex);
|
|
277
|
+
});
|
|
278
|
+
for (const [valueKey, count] of counts) {
|
|
279
|
+
if (count !== 1) index.delete(valueKey);
|
|
280
|
+
}
|
|
281
|
+
return index;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function characterBigrams(value: string): Set<string> {
|
|
285
|
+
const normalized = value.toLocaleLowerCase().replace(/\s+/g, " ").trim();
|
|
286
|
+
if (normalized.length < 2) return new Set(normalized ? [normalized] : []);
|
|
287
|
+
const result = new Set<string>();
|
|
288
|
+
for (let index = 0; index < normalized.length - 1; index++) {
|
|
289
|
+
result.add(normalized.slice(index, index + 2));
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function editSimilarity(left: string, right: string): number {
|
|
295
|
+
const leftBigrams = characterBigrams(left);
|
|
296
|
+
const rightBigrams = characterBigrams(right);
|
|
297
|
+
if (leftBigrams.size === 0 || rightBigrams.size === 0) return 0;
|
|
298
|
+
let shared = 0;
|
|
299
|
+
for (const bigram of leftBigrams) {
|
|
300
|
+
if (rightBigrams.has(bigram)) shared++;
|
|
301
|
+
}
|
|
302
|
+
return (2 * shared) / (leftBigrams.size + rightBigrams.size);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function reconcileBlocksFieldIdentity(args: {
|
|
306
|
+
documentId: string;
|
|
307
|
+
propertyId: string;
|
|
308
|
+
previous: StoredBlocksFieldIdentity;
|
|
309
|
+
markdown: string;
|
|
310
|
+
createId: () => string;
|
|
311
|
+
}): StoredBlocksFieldIdentity {
|
|
312
|
+
const nextRevision = args.previous.revision + 1;
|
|
313
|
+
const snapshots = snapshotMarkdown(args.markdown);
|
|
314
|
+
const previousLive = args.previous.blocks.filter(
|
|
315
|
+
(block) => block.state === "live",
|
|
316
|
+
);
|
|
317
|
+
const previousDeleted = args.previous.blocks.filter(
|
|
318
|
+
(block) => block.state === "deleted",
|
|
319
|
+
);
|
|
320
|
+
const matchedPrevious = new Set<number>();
|
|
321
|
+
const assigned = new Map<number, StoredBlocksFieldBlock>();
|
|
322
|
+
const usedIds = new Set(args.previous.blocks.map((block) => block.id));
|
|
323
|
+
|
|
324
|
+
const previousExact = uniqueIndexByKey(
|
|
325
|
+
previousLive,
|
|
326
|
+
(block) => `${block.kind}\0${block.contentHash}`,
|
|
327
|
+
);
|
|
328
|
+
const nextExact = uniqueIndexByKey(
|
|
329
|
+
snapshots,
|
|
330
|
+
(snapshot) => `${snapshot.kind}\0${snapshot.contentHash}`,
|
|
331
|
+
);
|
|
332
|
+
for (const [key, previousIndex] of previousExact) {
|
|
333
|
+
const nextIndex = nextExact.get(key);
|
|
334
|
+
if (nextIndex === undefined) continue;
|
|
335
|
+
matchedPrevious.add(previousIndex);
|
|
336
|
+
assigned.set(nextIndex, previousLive[previousIndex]!);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Split keeps the leading fragment's ID; merge keeps the receiving block's
|
|
340
|
+
// ID. These are the only cardinality-changing cases where position conveys
|
|
341
|
+
// more identity than text similarity.
|
|
342
|
+
const unmatchedKinds = new Set([
|
|
343
|
+
...previousLive
|
|
344
|
+
.filter((_block, index) => !matchedPrevious.has(index))
|
|
345
|
+
.map((block) => block.kind),
|
|
346
|
+
...snapshots
|
|
347
|
+
.filter((_snapshot, index) => !assigned.has(index))
|
|
348
|
+
.map((snapshot) => snapshot.kind),
|
|
349
|
+
]);
|
|
350
|
+
for (const kind of unmatchedKinds) {
|
|
351
|
+
const previousIndexes = previousLive.flatMap((block, index) =>
|
|
352
|
+
!matchedPrevious.has(index) && block.kind === kind ? [index] : [],
|
|
353
|
+
);
|
|
354
|
+
const nextIndexes = snapshots.flatMap((snapshot, index) =>
|
|
355
|
+
!assigned.has(index) && snapshot.kind === kind ? [index] : [],
|
|
356
|
+
);
|
|
357
|
+
if (previousIndexes.length !== 1 && nextIndexes.length !== 1) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (previousIndexes.length === 0 || nextIndexes.length === 0) continue;
|
|
361
|
+
const samePosition = nextIndexes.find(
|
|
362
|
+
(nextIndex) =>
|
|
363
|
+
snapshots[nextIndex]!.position ===
|
|
364
|
+
previousLive[previousIndexes[0]!]!.position,
|
|
365
|
+
);
|
|
366
|
+
if (samePosition === undefined) continue;
|
|
367
|
+
matchedPrevious.add(previousIndexes[0]!);
|
|
368
|
+
assigned.set(samePosition, previousLive[previousIndexes[0]!]!);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// A reorder and a text edit can happen in one editor transaction. Pair only
|
|
372
|
+
// mutual, sufficiently similar best matches before considering position; an
|
|
373
|
+
// ambiguous match is left for the conservative positional rule below.
|
|
374
|
+
const bestNextForPrevious = new Map<
|
|
375
|
+
number,
|
|
376
|
+
{ index: number; score: number }
|
|
377
|
+
>();
|
|
378
|
+
const bestPreviousForNext = new Map<
|
|
379
|
+
number,
|
|
380
|
+
{ index: number; score: number }
|
|
381
|
+
>();
|
|
382
|
+
for (
|
|
383
|
+
let previousIndex = 0;
|
|
384
|
+
previousIndex < previousLive.length;
|
|
385
|
+
previousIndex++
|
|
386
|
+
) {
|
|
387
|
+
if (matchedPrevious.has(previousIndex)) continue;
|
|
388
|
+
const previous = previousLive[previousIndex]!;
|
|
389
|
+
for (let nextIndex = 0; nextIndex < snapshots.length; nextIndex++) {
|
|
390
|
+
if (assigned.has(nextIndex)) continue;
|
|
391
|
+
const snapshot = snapshots[nextIndex]!;
|
|
392
|
+
if (previous.kind !== snapshot.kind) continue;
|
|
393
|
+
const score = editSimilarity(previous.markdown, snapshot.markdown);
|
|
394
|
+
if (score < 0.45) continue;
|
|
395
|
+
const previousBest = bestNextForPrevious.get(previousIndex);
|
|
396
|
+
if (!previousBest || score > previousBest.score) {
|
|
397
|
+
bestNextForPrevious.set(previousIndex, { index: nextIndex, score });
|
|
398
|
+
}
|
|
399
|
+
const nextBest = bestPreviousForNext.get(nextIndex);
|
|
400
|
+
if (!nextBest || score > nextBest.score) {
|
|
401
|
+
bestPreviousForNext.set(nextIndex, { index: previousIndex, score });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const [previousIndex, nextBest] of bestNextForPrevious) {
|
|
406
|
+
const previousBest = bestPreviousForNext.get(nextBest.index);
|
|
407
|
+
if (previousBest?.index !== previousIndex) continue;
|
|
408
|
+
matchedPrevious.add(previousIndex);
|
|
409
|
+
assigned.set(nextBest.index, previousLive[previousIndex]!);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (let nextIndex = 0; nextIndex < snapshots.length; nextIndex++) {
|
|
413
|
+
if (assigned.has(nextIndex)) continue;
|
|
414
|
+
const snapshot = snapshots[nextIndex]!;
|
|
415
|
+
const samePosition = previousLive.findIndex(
|
|
416
|
+
(block, previousIndex) =>
|
|
417
|
+
!matchedPrevious.has(previousIndex) &&
|
|
418
|
+
block.kind === snapshot.kind &&
|
|
419
|
+
block.position === snapshot.position,
|
|
420
|
+
);
|
|
421
|
+
if (samePosition !== -1) {
|
|
422
|
+
matchedPrevious.add(samePosition);
|
|
423
|
+
assigned.set(nextIndex, previousLive[samePosition]!);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const sameKind = previousLive.findIndex(
|
|
427
|
+
(block, previousIndex) =>
|
|
428
|
+
!matchedPrevious.has(previousIndex) && block.kind === snapshot.kind,
|
|
429
|
+
);
|
|
430
|
+
if (sameKind !== -1) {
|
|
431
|
+
matchedPrevious.add(sameKind);
|
|
432
|
+
assigned.set(nextIndex, previousLive[sameKind]!);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const recoverable = uniqueIndexByKey(
|
|
437
|
+
previousDeleted,
|
|
438
|
+
(block) => `${block.kind}\0${block.contentHash}`,
|
|
439
|
+
);
|
|
440
|
+
const recoveredIds = new Set<string>();
|
|
441
|
+
const idByPath = new Map<string, string>();
|
|
442
|
+
const nextBlocks = snapshots.map((snapshot, nextIndex) => {
|
|
443
|
+
let previous = assigned.get(nextIndex);
|
|
444
|
+
if (!previous) {
|
|
445
|
+
const recoveredIndex = recoverable.get(
|
|
446
|
+
`${snapshot.kind}\0${snapshot.contentHash}`,
|
|
447
|
+
);
|
|
448
|
+
if (recoveredIndex !== undefined) {
|
|
449
|
+
previous = previousDeleted[recoveredIndex];
|
|
450
|
+
if (previous) {
|
|
451
|
+
recoveredIds.add(previous.id);
|
|
452
|
+
recoverable.delete(`${snapshot.kind}\0${snapshot.contentHash}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
let id =
|
|
457
|
+
previous?.id ??
|
|
458
|
+
fieldScopedBlockId(
|
|
459
|
+
args.previous.fieldId,
|
|
460
|
+
snapshot.preferredId ?? args.createId(),
|
|
461
|
+
);
|
|
462
|
+
while (usedIds.has(id) && id !== previous?.id) {
|
|
463
|
+
id = fieldScopedBlockId(args.previous.fieldId, args.createId());
|
|
464
|
+
}
|
|
465
|
+
usedIds.add(id);
|
|
466
|
+
idByPath.set(snapshot.path, id);
|
|
467
|
+
return {
|
|
468
|
+
id,
|
|
469
|
+
parentId: snapshot.parentPath
|
|
470
|
+
? (idByPath.get(snapshot.parentPath) ?? null)
|
|
471
|
+
: null,
|
|
472
|
+
kind: snapshot.kind,
|
|
473
|
+
position: snapshot.position,
|
|
474
|
+
addressable: snapshot.addressable,
|
|
475
|
+
contentHash: snapshot.contentHash,
|
|
476
|
+
markdown: snapshot.markdown,
|
|
477
|
+
state: "live" as const,
|
|
478
|
+
deletedAtRevision: null,
|
|
479
|
+
recoveredAtRevision: recoveredIds.has(id) ? nextRevision : null,
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const deletedNow = previousLive
|
|
484
|
+
.filter((_block, index) => !matchedPrevious.has(index))
|
|
485
|
+
.map((block) => ({
|
|
486
|
+
...block,
|
|
487
|
+
state: "deleted" as const,
|
|
488
|
+
deletedAtRevision: nextRevision,
|
|
489
|
+
recoveredAtRevision: null,
|
|
490
|
+
}));
|
|
491
|
+
const oldestRecoverableRevision =
|
|
492
|
+
nextRevision - BLOCK_TOMBSTONE_REVISION_WINDOW;
|
|
493
|
+
const retainedTombstones = [...previousDeleted, ...deletedNow]
|
|
494
|
+
.filter(
|
|
495
|
+
(block) =>
|
|
496
|
+
!recoveredIds.has(block.id) &&
|
|
497
|
+
block.deletedAtRevision !== null &&
|
|
498
|
+
block.deletedAtRevision > oldestRecoverableRevision,
|
|
499
|
+
)
|
|
500
|
+
.sort(
|
|
501
|
+
(left, right) =>
|
|
502
|
+
(right.deletedAtRevision ?? 0) - (left.deletedAtRevision ?? 0),
|
|
503
|
+
)
|
|
504
|
+
.slice(0, MAX_BLOCK_TOMBSTONES_PER_FIELD);
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
fieldId: args.previous.fieldId,
|
|
508
|
+
revision: nextRevision,
|
|
509
|
+
contentHash: blocksContentHash(args.markdown),
|
|
510
|
+
blocks: [...nextBlocks, ...retainedTombstones],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function exposeBlocksFieldIdentity(
|
|
515
|
+
stored: StoredBlocksFieldIdentity,
|
|
516
|
+
markdown: string,
|
|
517
|
+
): BlocksFieldIdentity {
|
|
518
|
+
const currentHash = blocksContentHash(markdown);
|
|
519
|
+
if (currentHash === stored.contentHash) {
|
|
520
|
+
return publicIdentity(stored, "materialized");
|
|
521
|
+
}
|
|
522
|
+
let provisionalIndex = 0;
|
|
523
|
+
const reconciled = reconcileBlocksFieldIdentity({
|
|
524
|
+
documentId: "stale-read",
|
|
525
|
+
propertyId: stored.fieldId,
|
|
526
|
+
previous: stored,
|
|
527
|
+
markdown,
|
|
528
|
+
createId: () =>
|
|
529
|
+
`block_${hashString(`${stored.fieldId}\0${markdown}\0${provisionalIndex++}`)}`,
|
|
530
|
+
});
|
|
531
|
+
return publicIdentity({ ...reconciled, revision: stored.revision }, "stale");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function materializeLegacyBlocksFieldIdentity(args: {
|
|
535
|
+
documentId: string;
|
|
536
|
+
propertyId: string;
|
|
537
|
+
markdown: string;
|
|
538
|
+
}): StoredBlocksFieldIdentity {
|
|
539
|
+
const publicState = legacyBlocksFieldIdentity(args);
|
|
540
|
+
const snapshots = snapshotMarkdown(args.markdown);
|
|
541
|
+
return {
|
|
542
|
+
fieldId: publicState.fieldId,
|
|
543
|
+
revision: 0,
|
|
544
|
+
contentHash: publicState.contentHash,
|
|
545
|
+
blocks: publicState.blocks.map((block, index) => ({
|
|
546
|
+
...block,
|
|
547
|
+
contentHash: snapshots[index]!.contentHash,
|
|
548
|
+
markdown: snapshots[index]!.markdown,
|
|
549
|
+
state: "live",
|
|
550
|
+
deletedAtRevision: null,
|
|
551
|
+
recoveredAtRevision: null,
|
|
552
|
+
})),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlocksFieldIdentity } from "./blocks-field-identity.js";
|
|
1
2
|
import { matchInlineMathAt } from "./inline-math.js";
|
|
2
3
|
import { KATEX_STYLESHEET_URL, renderMathToHtml } from "./math-rendering.js";
|
|
3
4
|
|
|
@@ -9,6 +10,16 @@ export interface DocumentExportInput {
|
|
|
9
10
|
content?: string | null;
|
|
10
11
|
updatedAt?: string | null;
|
|
11
12
|
format: DocumentExportFormat;
|
|
13
|
+
blocksFields?: BlocksFieldExport[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BlocksFieldExport {
|
|
17
|
+
databaseId: string;
|
|
18
|
+
propertyId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
position: number;
|
|
21
|
+
markdown: string;
|
|
22
|
+
identity: BlocksFieldIdentity;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
export interface DocumentExportPayload {
|
|
@@ -19,6 +30,15 @@ export interface DocumentExportPayload {
|
|
|
19
30
|
mimeType: string;
|
|
20
31
|
content: string;
|
|
21
32
|
print: boolean;
|
|
33
|
+
blocksFields?: BlocksFieldExport[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function serializeBlocksFieldsManifest(fields: BlocksFieldExport[]): string {
|
|
37
|
+
return JSON.stringify({ version: 1, fields })
|
|
38
|
+
.replace(/</g, "\\u003c")
|
|
39
|
+
.replace(/>/g, "\\u003e")
|
|
40
|
+
.replace(/&/g, "\\u0026")
|
|
41
|
+
.replace(/--/g, "\\u002d\\u002d");
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
const EXTENSION_BY_FORMAT: Record<DocumentExportFormat, string> = {
|
|
@@ -638,7 +658,10 @@ export function buildDocumentExport(
|
|
|
638
658
|
const filename = exportFilename(title, input.format);
|
|
639
659
|
const markdown = markdownWithTitle(title, input.content);
|
|
640
660
|
const isHtmlLike = input.format === "html" || input.format === "pdf";
|
|
641
|
-
const
|
|
661
|
+
const manifest = input.blocksFields?.length
|
|
662
|
+
? serializeBlocksFieldsManifest(input.blocksFields)
|
|
663
|
+
: null;
|
|
664
|
+
let content = isHtmlLike
|
|
642
665
|
? buildHtmlDocument({
|
|
643
666
|
title,
|
|
644
667
|
content: input.content ?? "",
|
|
@@ -646,6 +669,14 @@ export function buildDocumentExport(
|
|
|
646
669
|
print: input.format === "pdf",
|
|
647
670
|
})
|
|
648
671
|
: markdown;
|
|
672
|
+
if (manifest) {
|
|
673
|
+
content = isHtmlLike
|
|
674
|
+
? content.replace(
|
|
675
|
+
"</body>",
|
|
676
|
+
`<script type="application/json" id="agent-native-blocks">${manifest}</script>\n</body>`,
|
|
677
|
+
)
|
|
678
|
+
: `${content.trimEnd()}\n\n<!-- agent-native-blocks:${manifest} -->\n`;
|
|
679
|
+
}
|
|
649
680
|
|
|
650
681
|
return {
|
|
651
682
|
id: input.id,
|
|
@@ -655,5 +686,6 @@ export function buildDocumentExport(
|
|
|
655
686
|
mimeType: MIME_BY_FORMAT[input.format],
|
|
656
687
|
content,
|
|
657
688
|
print: input.format === "pdf",
|
|
689
|
+
...(input.blocksFields?.length ? { blocksFields: input.blocksFields } : {}),
|
|
658
690
|
};
|
|
659
691
|
}
|
package/dist/action.d.ts
CHANGED
|
@@ -135,6 +135,28 @@ export interface AgentActionStopOptions {
|
|
|
135
135
|
/** Optional short tool-result text. Defaults to the user-facing message. */
|
|
136
136
|
toolResult?: string;
|
|
137
137
|
}
|
|
138
|
+
export interface ActionContractErrorOptions {
|
|
139
|
+
/** Stable machine-readable code safe to expose on every action transport. */
|
|
140
|
+
errorCode: string;
|
|
141
|
+
/** Safe structured context for callers. Never include secrets or raw driver errors. */
|
|
142
|
+
details?: Record<string, unknown>;
|
|
143
|
+
/** HTTP status for the action route. Contract conflicts normally use 409. */
|
|
144
|
+
statusCode?: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A deterministic, caller-correctable action contract failure.
|
|
148
|
+
*
|
|
149
|
+
* Unlike an ordinary Error, its code and explicitly safe details survive the
|
|
150
|
+
* framework HTTP transport. Internal failures remain generic 500 responses.
|
|
151
|
+
*/
|
|
152
|
+
export declare class ActionContractError extends Error {
|
|
153
|
+
readonly actionContractError = true;
|
|
154
|
+
readonly errorCode: string;
|
|
155
|
+
readonly details?: Record<string, unknown>;
|
|
156
|
+
readonly statusCode: number;
|
|
157
|
+
constructor(message: string, options: ActionContractErrorOptions);
|
|
158
|
+
}
|
|
159
|
+
export declare function isActionContractError(error: unknown): error is ActionContractError;
|
|
138
160
|
/**
|
|
139
161
|
* Throw from an action when the agent should stop the current turn instead of
|
|
140
162
|
* feeding the failure back to the model for another retry.
|
package/dist/action.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { normalizeActionChatUIConfig, } from "./action-ui.js";
|
|
2
2
|
import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* A deterministic, caller-correctable action contract failure.
|
|
5
|
+
*
|
|
6
|
+
* Unlike an ordinary Error, its code and explicitly safe details survive the
|
|
7
|
+
* framework HTTP transport. Internal failures remain generic 500 responses.
|
|
8
|
+
*/
|
|
9
|
+
export class ActionContractError extends Error {
|
|
10
|
+
actionContractError = true;
|
|
11
|
+
errorCode;
|
|
12
|
+
details;
|
|
13
|
+
statusCode;
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ActionContractError";
|
|
17
|
+
this.errorCode = options.errorCode;
|
|
18
|
+
this.details = options.details;
|
|
19
|
+
this.statusCode = options.statusCode ?? 409;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function isActionContractError(error) {
|
|
23
|
+
return (error instanceof ActionContractError ||
|
|
24
|
+
(!!error &&
|
|
25
|
+
typeof error === "object" &&
|
|
26
|
+
error.actionContractError ===
|
|
27
|
+
true &&
|
|
28
|
+
typeof error.errorCode === "string"));
|
|
29
|
+
}
|
|
3
30
|
/**
|
|
4
31
|
* Throw from an action when the agent should stop the current turn instead of
|
|
5
32
|
* feeding the failure back to the model for another retry.
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|