@agent-native/core 0.211.0 → 0.211.1-nightly-20261020213022

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.
Files changed (52) hide show
  1. package/dist/cli/template-baseline.js +18 -5
  2. package/dist/cli/template-sync.js +100 -19
  3. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  4. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  5. package/dist/collab/awareness.d.ts +2 -2
  6. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  7. package/dist/collab/bounded-task-bundle.js +39 -0
  8. package/dist/collab/bounded-task-source.d.ts +14 -0
  9. package/dist/collab/bounded-task-source.js +8 -0
  10. package/dist/collab/bounded-tasks.d.ts +67 -0
  11. package/dist/collab/bounded-tasks.js +439 -0
  12. package/dist/collab/index.d.ts +5 -2
  13. package/dist/collab/index.js +5 -2
  14. package/dist/collab/live-copy-tasks.d.ts +80 -0
  15. package/dist/collab/live-copy-tasks.js +330 -0
  16. package/dist/collab/live-copy.d.ts +188 -0
  17. package/dist/collab/live-copy.js +415 -0
  18. package/dist/collab/routes.d.ts +6 -1
  19. package/dist/collab/routes.js +24 -8
  20. package/dist/collab/storage.d.ts +53 -1
  21. package/dist/collab/storage.js +93 -5
  22. package/dist/collab/ydoc-manager.d.ts +92 -3
  23. package/dist/collab/ydoc-manager.js +447 -38
  24. package/dist/db/client.d.ts +6 -0
  25. package/dist/db/client.js +25 -5
  26. package/dist/deploy/build.js +2 -0
  27. package/dist/extensions/actions.js +1 -0
  28. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  29. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  30. package/dist/mcp/agent-connected.js +5 -1
  31. package/dist/mcp/build-server.js +39 -10
  32. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  33. package/dist/org/handlers.d.ts +1 -1
  34. package/dist/org/index.d.ts +1 -1
  35. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  36. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  37. package/dist/resources/handlers.d.ts +1 -1
  38. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  39. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  40. package/dist/server/collab-plugin.d.ts +10 -1
  41. package/dist/server/collab-plugin.js +89 -20
  42. package/dist/server/realtime-token.d.ts +1 -1
  43. package/dist/shared/optional-node-builtins.d.ts +1 -0
  44. package/dist/shared/optional-node-builtins.js +3 -0
  45. package/dist/tracking/providers.d.ts +1 -0
  46. package/dist/tracking/providers.js +2 -1
  47. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  48. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  49. package/dist/vite/bounded-task-plugin.js +44 -0
  50. package/dist/vite/client.js +2 -0
  51. package/dist/vitest-config.js +3 -0
  52. package/package.json +4 -3
@@ -0,0 +1,330 @@
1
+ import * as Y from "yjs";
2
+ // The worker half of a live copy. Every Yjs operation on a live copy runs
3
+ // here, inside a bounded task, and never on the request thread: integrating a
4
+ // crafted update costs time quadratic in nesting depth (a 6,000-deep tree
5
+ // takes seconds), and only a worker can be stopped when it overruns. Import
6
+ // nothing here that a worker bundle cannot carry.
7
+ export const LIVE_COPY_TASK = {
8
+ read: "liveCopy.read",
9
+ apply: "liveCopy.apply",
10
+ seed: "liveCopy.seed",
11
+ edit: "liveCopy.edit",
12
+ };
13
+ class TaskFailure extends Error {
14
+ code;
15
+ reason;
16
+ constructor(code, reason, message) {
17
+ super(message);
18
+ this.code = code;
19
+ this.reason = reason;
20
+ }
21
+ }
22
+ function fail(code, reason, message) {
23
+ throw new TaskFailure(code, reason, message);
24
+ }
25
+ function isRefusal(value) {
26
+ return (typeof value === "object" &&
27
+ value !== null &&
28
+ value.kind === "refused" &&
29
+ typeof value.reason === "string");
30
+ }
31
+ function describeRefusal(refusal) {
32
+ return `${refusal.reason}${refusal.detail ? ` (${refusal.detail})` : ""}`;
33
+ }
34
+ function hasLiveContent(type) {
35
+ for (let item = type._start; item !== null; item = item.right) {
36
+ if (!item.deleted)
37
+ return true;
38
+ }
39
+ for (const item of type._map.values()) {
40
+ if (!item.deleted)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ function fieldHasContent(doc, field) {
46
+ const type = doc.share.get(field);
47
+ return type !== undefined && hasLiveContent(type);
48
+ }
49
+ /**
50
+ * Content outside the field would be invisible to derive, so the stored text
51
+ * would silently omit it. Refuse instead.
52
+ */
53
+ function assertOnlyField(doc, field) {
54
+ const stray = [];
55
+ for (const [name, type] of doc.share) {
56
+ if (name !== field && hasLiveContent(type))
57
+ stray.push(name);
58
+ }
59
+ if (stray.length > 0) {
60
+ fail("unsupported_content", stray.join(","), `holds content outside "${field}": ${stray.join(", ")}`);
61
+ }
62
+ }
63
+ /**
64
+ * Yjs parks structs and deletions whose dependencies are missing instead of
65
+ * applying them; derive would never see them.
66
+ */
67
+ function hasParked(doc) {
68
+ return doc.store.pendingStructs !== null || doc.store.pendingDs !== null;
69
+ }
70
+ function assertIntegrated(doc, what) {
71
+ if (hasParked(doc)) {
72
+ fail("malformed_state", "missing_dependencies", `the ${what} depends on changes it does not have`);
73
+ }
74
+ }
75
+ /**
76
+ * Applies a whole update and reports whether every byte was read.
77
+ * `Y.applyUpdate` stops after the delete set and ignores the rest, so junk
78
+ * after a valid prefix would pass as state. The reader is lib0's decoder
79
+ * shape, which Yjs reads structurally; lib0 is not a dependency of core.
80
+ */
81
+ function readWhole(doc, bytes) {
82
+ const decoder = { arr: bytes, pos: 0 };
83
+ Y.readUpdate(decoder, doc, null);
84
+ return decoder.pos === bytes.length;
85
+ }
86
+ function catchUp(doc) {
87
+ const stateVector = Y.encodeStateVector(doc);
88
+ return { stateVector, deletions: Y.encodeStateAsUpdate(doc, stateVector) };
89
+ }
90
+ function load(state, field) {
91
+ const doc = new Y.Doc();
92
+ try {
93
+ if (state !== null && state.length > 0) {
94
+ let whole = false;
95
+ try {
96
+ whole = readWhole(doc, state);
97
+ }
98
+ catch {
99
+ whole = false;
100
+ }
101
+ if (!whole) {
102
+ fail("malformed_state", "yjs", "the stored state is not a Yjs update");
103
+ }
104
+ assertIntegrated(doc, "stored state");
105
+ }
106
+ assertOnlyField(doc, field);
107
+ return doc;
108
+ }
109
+ catch (error) {
110
+ doc.destroy();
111
+ throw error;
112
+ }
113
+ }
114
+ function watchChanges(doc) {
115
+ let changed = false;
116
+ const onUpdate = () => {
117
+ changed = true;
118
+ };
119
+ doc.on("update", onUpdate);
120
+ return {
121
+ changed: () => changed,
122
+ stop: () => doc.off("update", onUpdate),
123
+ };
124
+ }
125
+ function applyChange(doc, update, what) {
126
+ const watch = watchChanges(doc);
127
+ let whole = false;
128
+ try {
129
+ whole = readWhole(doc, update);
130
+ }
131
+ catch {
132
+ whole = false;
133
+ }
134
+ finally {
135
+ watch.stop();
136
+ }
137
+ if (!whole) {
138
+ fail("malformed_state", "update", `the ${what} is not a Yjs update`);
139
+ }
140
+ assertIntegrated(doc, what);
141
+ return watch.changed();
142
+ }
143
+ function encode(doc, maxStateBytes) {
144
+ const state = Y.encodeStateAsUpdate(doc);
145
+ if (state.length > maxStateBytes) {
146
+ fail("state_too_large", String(state.length), `would be ${state.length} bytes; the limit is ${maxStateBytes}`);
147
+ }
148
+ return state;
149
+ }
150
+ async function derive(handlers, doc, field) {
151
+ const watch = watchChanges(doc);
152
+ let output;
153
+ try {
154
+ output = await handlers.derive(doc, field);
155
+ }
156
+ finally {
157
+ watch.stop();
158
+ }
159
+ // An edit here would make the stored text describe a state never stored.
160
+ // Parking an update emits no update event, so check for that too.
161
+ if (watch.changed() || hasParked(doc)) {
162
+ fail("derive_failed", "derive_edited", "derive edited the document");
163
+ }
164
+ if (typeof output === "string")
165
+ return output;
166
+ if (isRefusal(output)) {
167
+ fail("derive_refused", output.reason, `cannot be derived: ${describeRefusal(output)}`);
168
+ }
169
+ fail("derive_failed", "invalid_output", "derive returned neither text nor a refusal");
170
+ }
171
+ async function build(what, run) {
172
+ const output = await run();
173
+ if (output === undefined)
174
+ return;
175
+ if (isRefusal(output)) {
176
+ fail("build_refused", output.reason, `${what} refused: ${describeRefusal(output)}`);
177
+ }
178
+ fail("build_failed", "invalid_output", `${what} returned something other than nothing or a refusal`);
179
+ }
180
+ function guarded(run) {
181
+ return async (input) => {
182
+ try {
183
+ return await run(input);
184
+ }
185
+ catch (error) {
186
+ if (error instanceof TaskFailure) {
187
+ return {
188
+ kind: "failed",
189
+ code: error.code,
190
+ reason: error.reason,
191
+ message: error.message,
192
+ };
193
+ }
194
+ throw error;
195
+ }
196
+ };
197
+ }
198
+ /**
199
+ * The tasks a live copy's runner must host. Export the result as `tasks`
200
+ * from the module the runner bundles, alongside any tasks of your own.
201
+ */
202
+ export function defineLiveCopyTasks(handlers) {
203
+ if (typeof handlers?.derive !== "function") {
204
+ throw new TypeError("defineLiveCopyTasks needs a derive(doc, field) handler");
205
+ }
206
+ const edits = handlers.edits ?? {};
207
+ return {
208
+ [LIVE_COPY_TASK.read]: guarded(async (input) => {
209
+ const doc = load(input.state, input.field);
210
+ try {
211
+ if (!fieldHasContent(doc, input.field)) {
212
+ return { kind: "unseeded", ...catchUp(doc) };
213
+ }
214
+ if (!input.derive)
215
+ return { kind: "live", ...catchUp(doc) };
216
+ return {
217
+ kind: "live",
218
+ ...catchUp(doc),
219
+ text: await derive(handlers, doc, input.field),
220
+ };
221
+ }
222
+ finally {
223
+ doc.destroy();
224
+ }
225
+ }),
226
+ [LIVE_COPY_TASK.apply]: guarded(async (input) => {
227
+ const doc = load(input.state, input.field);
228
+ try {
229
+ if (!applyChange(doc, input.update, "update")) {
230
+ return { kind: "unchanged" };
231
+ }
232
+ assertOnlyField(doc, input.field);
233
+ // An empty field reads as unseeded, and a seed would then write over
234
+ // what the user deleted. Editors always keep one block.
235
+ if (!fieldHasContent(doc, input.field)) {
236
+ fail("unsupported_content", "empty", `the update would leave "${input.field}" empty`);
237
+ }
238
+ const state = encode(doc, input.maxStateBytes);
239
+ const caughtUp = catchUp(doc);
240
+ const text = await derive(handlers, doc, input.field);
241
+ return { kind: "changed", state, text, ...caughtUp };
242
+ }
243
+ finally {
244
+ doc.destroy();
245
+ }
246
+ }),
247
+ [LIVE_COPY_TASK.seed]: guarded(async (input) => {
248
+ const seed = handlers.seed;
249
+ if (typeof seed !== "function") {
250
+ fail("build_failed", "seed_unknown", "the task module defines no seed");
251
+ }
252
+ const doc = load(input.state, input.field);
253
+ try {
254
+ if (fieldHasContent(doc, input.field)) {
255
+ return {
256
+ kind: "live",
257
+ ...catchUp(doc),
258
+ text: await derive(handlers, doc, input.field),
259
+ };
260
+ }
261
+ const seeded = new Y.Doc();
262
+ let seedUpdate;
263
+ try {
264
+ await build("the seed", () => seed(seeded, input.field, input.source));
265
+ assertIntegrated(seeded, "seed");
266
+ seedUpdate = Y.encodeStateAsUpdate(seeded);
267
+ }
268
+ finally {
269
+ seeded.destroy();
270
+ }
271
+ const before = Y.encodeStateVector(doc);
272
+ applyChange(doc, seedUpdate, "seed");
273
+ assertOnlyField(doc, input.field);
274
+ if (!fieldHasContent(doc, input.field)) {
275
+ fail("build_refused", "empty", `the seed left "${input.field}" empty`);
276
+ }
277
+ const state = encode(doc, input.maxStateBytes);
278
+ const update = Y.encodeStateAsUpdate(doc, before);
279
+ const caughtUp = catchUp(doc);
280
+ const text = await derive(handlers, doc, input.field);
281
+ return { kind: "changed", state, text, update, ...caughtUp };
282
+ }
283
+ finally {
284
+ doc.destroy();
285
+ }
286
+ }),
287
+ [LIVE_COPY_TASK.edit]: guarded(async (input) => {
288
+ const edit = Object.prototype.hasOwnProperty.call(edits, input.edit)
289
+ ? edits[input.edit]
290
+ : undefined;
291
+ if (typeof edit !== "function") {
292
+ fail("build_failed", "edit_unknown", `the task module defines no edit named ${input.edit}`);
293
+ }
294
+ const doc = load(input.state, input.field);
295
+ try {
296
+ if (!fieldHasContent(doc, input.field)) {
297
+ fail("not_seeded", "unseeded", "is unseeded; seed it before editing");
298
+ }
299
+ const before = Y.encodeStateVector(doc);
300
+ const watch = watchChanges(doc);
301
+ try {
302
+ await build(`edit ${input.edit}`, () => edit(doc, input.field, input.input));
303
+ }
304
+ finally {
305
+ watch.stop();
306
+ }
307
+ // An edit that applies an update can leave part of it parked.
308
+ assertIntegrated(doc, `edit ${input.edit}`);
309
+ if (!watch.changed()) {
310
+ return {
311
+ kind: "unchanged",
312
+ text: await derive(handlers, doc, input.field),
313
+ };
314
+ }
315
+ assertOnlyField(doc, input.field);
316
+ if (!fieldHasContent(doc, input.field)) {
317
+ fail("build_refused", "empty", `edit ${input.edit} would leave "${input.field}" empty`);
318
+ }
319
+ const state = encode(doc, input.maxStateBytes);
320
+ const update = Y.encodeStateAsUpdate(doc, before);
321
+ const caughtUp = catchUp(doc);
322
+ const text = await derive(handlers, doc, input.field);
323
+ return { kind: "changed", state, text, update, ...caughtUp };
324
+ }
325
+ finally {
326
+ doc.destroy();
327
+ }
328
+ }),
329
+ };
330
+ }
@@ -0,0 +1,188 @@
1
+ import { type DbExec } from "../db/client.js";
2
+ import { type BoundedTaskRunner } from "./bounded-tasks.js";
3
+ import { type LiveCopyCatchUp } from "./live-copy-tasks.js";
4
+ import { type YDocStamp } from "./storage.js";
5
+ export declare const LIVE_COPY_DEFAULT_FIELD = "default";
6
+ export type LiveCopyErrorCode = "not_live_copy" | "not_seeded" | "write_refused" | "read_refused" | "malformed_state" | "unsupported_content" | "state_too_large" | "derive_refused" | "derive_failed" | "build_refused" | "build_failed" | "version_conflict";
7
+ export declare class LiveCopyError extends Error {
8
+ readonly code: LiveCopyErrorCode;
9
+ readonly docId: string;
10
+ /**
11
+ * Refusal reason from the task, the malformed-state reason, the bounded
12
+ * task error code, or the refused path, depending on `code`.
13
+ */
14
+ readonly reason?: string;
15
+ constructor(code: LiveCopyErrorCode, docId: string, message: string, options?: {
16
+ reason?: string;
17
+ cause?: unknown;
18
+ });
19
+ }
20
+ export interface LiveCopyCommit {
21
+ readonly docId: string;
22
+ readonly origin: "update" | "mutation" | "seed";
23
+ /** Null when this commit creates the `_collab_docs` row. */
24
+ readonly previousVersion: number | null;
25
+ readonly version: number;
26
+ readonly text: string;
27
+ readonly state: Uint8Array;
28
+ readonly requestSource?: string;
29
+ }
30
+ /**
31
+ * What `persist` reports. Returning nothing means the app's rows were
32
+ * derived; `derived: false` means it left them alone (say the document went
33
+ * back to its legacy writer), and the Yjs state still commits.
34
+ */
35
+ export type LiveCopyPersistOutcome = {
36
+ derived: true;
37
+ } | {
38
+ derived: false;
39
+ reason: string;
40
+ };
41
+ /**
42
+ * How a write left the app's derived rows. `unchanged` means nothing was
43
+ * committed, so persist did not run.
44
+ */
45
+ export interface LiveCopyOutcome {
46
+ liveCopy: "derived" | "not_derived" | "unchanged";
47
+ liveCopyReason?: string;
48
+ }
49
+ export interface LiveCopyOptions {
50
+ /**
51
+ * Stable identity. Defining a binding under the same name again, as a dev
52
+ * server does when it evaluates the app's module again, replaces the old
53
+ * one.
54
+ */
55
+ name: string;
56
+ /** Which collab documents are live copies. Checked on every write. */
57
+ appliesTo(docId: string): boolean | Promise<boolean>;
58
+ /**
59
+ * Hosts the live-copy tasks: its module exports
60
+ * `tasks = defineLiveCopyTasks({ derive, seed, edits })`.
61
+ */
62
+ runner: BoundedTaskRunner;
63
+ /** The one root type a live copy may hold. Default "default". */
64
+ field?: string;
65
+ /** Encoded state above this is refused. Default 4 MiB. */
66
+ maxStateBytes?: number;
67
+ /**
68
+ * Writes to one document allowed to wait while another commits. Beyond
69
+ * this, a write fails with `build_failed` / `queue_full` instead of
70
+ * holding its payload until its turn. Default 16.
71
+ */
72
+ maxWaitingWrites?: number;
73
+ /**
74
+ * Writes the app's derived stores inside the commit transaction. It runs
75
+ * before core updates `_collab_docs`, so app row locks are always taken
76
+ * first. A throw rolls the commit back and reaches the caller unchanged.
77
+ * It may run more than once per write when a version conflict retries.
78
+ * It runs in the caller's async context, and `getDbExec()` inside it is
79
+ * `tx`; a Drizzle `getDb()` handle is not, so query through `tx`.
80
+ */
81
+ persist?(tx: DbExec, commit: LiveCopyCommit): Promise<void | LiveCopyPersistOutcome>;
82
+ }
83
+ export interface LiveCopyBinding {
84
+ readonly name: string;
85
+ readonly appliesTo: (docId: string) => boolean | Promise<boolean>;
86
+ readonly runner: BoundedTaskRunner;
87
+ readonly field: string;
88
+ readonly maxStateBytes: number;
89
+ readonly maxWaitingWrites: number;
90
+ readonly persist?: (tx: DbExec, commit: LiveCopyCommit) => Promise<void | LiveCopyPersistOutcome>;
91
+ }
92
+ /**
93
+ * Declares which collab documents are live copies and how their text is
94
+ * derived. While a binding exists, core's other write paths (text, JSON,
95
+ * search-replace, prepared mutations, and the text/JSON/XML seeders) refuse
96
+ * documents it covers.
97
+ */
98
+ export declare function defineLiveCopy(options: LiveCopyOptions): LiveCopyBinding;
99
+ /** Synchronous, so a caller can keep main's behavior when none is defined. */
100
+ export declare function liveCopiesDefined(): boolean;
101
+ /**
102
+ * Removes a binding so its documents take ordinary collab writes again. A
103
+ * binding already replaced under its name leaves the replacement in place.
104
+ */
105
+ export declare function releaseLiveCopy(binding: LiveCopyBinding): void;
106
+ export declare function liveCopyBindingFor(docId: string): Promise<LiveCopyBinding | null>;
107
+ /**
108
+ * Throws `write_refused` when a live copy covers `docId`. With no binding
109
+ * defined it returns synchronously, so a write that has not taken its lock
110
+ * yet keeps its place in line.
111
+ */
112
+ export declare function refuseLiveCopyWrite(docId: string, path: string): Promise<void> | undefined;
113
+ export declare function liveCopyWriteRefused(docId: string, path: string): LiveCopyError;
114
+ export declare function assertLiveCopy(binding: LiveCopyBinding, docId: string): Promise<void>;
115
+ /** Throws unless `binding` is still the one registered under its name. */
116
+ export declare function assertActiveLiveCopy(binding: LiveCopyBinding, docId: string): void;
117
+ /**
118
+ * A commit updates this instance's cache and notifies connected editors as
119
+ * soon as it lands, so it must own its transaction: inside a caller's, a
120
+ * later rollback would leave editors holding state that was never saved.
121
+ */
122
+ export declare function assertOwnTransaction(docId: string, path: string): void;
123
+ export type StoredLiveCopy = {
124
+ kind: "absent";
125
+ } | {
126
+ kind: "stored";
127
+ state: Uint8Array;
128
+ version: number;
129
+ digest: string;
130
+ };
131
+ /**
132
+ * The stored bytes, read strictly: unreadable base64 or a bad version throws
133
+ * `malformed_state` instead of reading as empty. The bytes are not decoded
134
+ * as Yjs here; the tasks do that in the runner.
135
+ */
136
+ export declare function readStoredLiveCopy(binding: LiveCopyBinding, client: DbExec, docId: string): Promise<StoredLiveCopy>;
137
+ export interface LiveCopyChange {
138
+ kind: "changed";
139
+ state: Uint8Array;
140
+ text: string;
141
+ /** What this change added to the state it started from. */
142
+ update: Uint8Array;
143
+ catchUp: LiveCopyCatchUp;
144
+ }
145
+ /** Validates stored state in the runner; derives its text when asked. */
146
+ export declare function readLiveCopyState(binding: LiveCopyBinding, docId: string, state: Uint8Array, derive: boolean): Promise<{
147
+ kind: "unseeded";
148
+ catchUp: LiveCopyCatchUp;
149
+ } | {
150
+ kind: "live";
151
+ text: string | null;
152
+ catchUp: LiveCopyCatchUp;
153
+ }>;
154
+ /** Merges a client update in the runner; null state means absent. */
155
+ export declare function applyLiveCopyUpdate(binding: LiveCopyBinding, docId: string, state: Uint8Array | null, update: Uint8Array): Promise<{
156
+ kind: "unchanged";
157
+ } | {
158
+ kind: "changed";
159
+ state: Uint8Array;
160
+ text: string;
161
+ catchUp: LiveCopyCatchUp;
162
+ }>;
163
+ /** Seeds an absent or unseeded copy; a live one comes back with its text. */
164
+ export declare function seedLiveCopyState(binding: LiveCopyBinding, docId: string, state: Uint8Array | null, source: string): Promise<{
165
+ kind: "live";
166
+ text: string;
167
+ } | LiveCopyChange>;
168
+ /** Runs a named edit from the runner's module on seeded state. */
169
+ export declare function editLiveCopyState(binding: LiveCopyBinding, docId: string, state: Uint8Array, edit: string, editInput: unknown): Promise<{
170
+ kind: "unchanged";
171
+ text: string;
172
+ } | LiveCopyChange>;
173
+ /** A caller's own write, added to the transaction of one live-copy write. */
174
+ export type LiveCopyCallPersist = (tx: DbExec) => Promise<void>;
175
+ export declare function sameStamp(a: YDocStamp | null, b: YDocStamp | null): boolean;
176
+ /**
177
+ * Commits a derived state: the binding's persist hook, the caller's, then
178
+ * the `_collab_docs` write, in one transaction, at the row `base` names
179
+ * (null: no row). Returns null when another writer changed the row first;
180
+ * nothing is written in that case.
181
+ */
182
+ export declare function commitLiveCopy(binding: LiveCopyBinding, commit: LiveCopyCommit, base: YDocStamp | null, callPersist?: LiveCopyCallPersist): Promise<LiveCopyOutcome | null>;
183
+ /**
184
+ * Runs a caller's persist by itself, for a write that committed nothing,
185
+ * while the copy is still the row `base` names. Returns false, with nothing
186
+ * written, when another writer changed it first.
187
+ */
188
+ export declare function persistLiveCopyCall(binding: LiveCopyBinding, docId: string, base: YDocStamp, callPersist: LiveCopyCallPersist): Promise<boolean>;