@agent-native/core 0.120.3 → 0.120.4

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.
@@ -33,6 +33,11 @@ export function getEditorMarkdown(editor: Editor): string {
33
33
  * is identical, so skipping it is safe by construction.
34
34
  */
35
35
  const EMITTED_RING_MAX = 16;
36
+ // The hosted awareness transport polls every 2s when its SSE gateway cannot
37
+ // forward presence. Give that first snapshot one poll plus margin before an
38
+ // empty SQL value is allowed to clear a nonempty Y.Doc. This is the same settle
39
+ // window used below when a known peer may deliver an edit through Yjs.
40
+ const PEER_SETTLE_MS = 2500;
36
41
  function pushEmittedRing(ring: string[], value: string): void {
37
42
  if (!value) return;
38
43
  if (ring[ring.length - 1] === value) return;
@@ -264,6 +269,11 @@ export function useCollabReconcile({
264
269
  // arrives via Yjs, so external markdown reconcile must defer (avoid applying
265
270
  // the same change through both Yjs and setContent).
266
271
  const peerCountRef = useRef(0);
272
+ // Briefly gates the first empty-SQL reconcile while Collaboration projects
273
+ // a nonempty fragment. `seededRef` is still released immediately so a real
274
+ // first keystroke can persist; this ref only postpones the ambiguous clear
275
+ // decision until we can distinguish an active/local edit from stale CRDT.
276
+ const emptySnapshotDecisionPendingRef = useRef(false);
267
277
  useEffect(() => {
268
278
  if (!collab || !awareness || !ydoc) {
269
279
  setIsLeadClient(true);
@@ -299,25 +309,73 @@ export function useCollabReconcile({
299
309
  if (!collab || !editor || editor.isDestroyed || !ydoc) return;
300
310
  if (seededRef.current) return;
301
311
  if (!collabSynced) return;
302
- if (!isLeadClient) return;
303
- if (!value.trim()) return;
304
- const fragment = ydoc.getXmlFragment("default");
305
- const currentMarkdown = getMarkdown(editor);
306
- // Seed only when the shared doc is genuinely empty — either the fragment has
307
- // no nodes yet, or it holds no semantic markdown (an empty paragraph, or an
308
- // app's sentinel-empty filler via a custom `shouldSeed`).
309
- if (
310
- !shouldSeed({ value, currentMarkdown, fragmentLength: fragment.length })
311
- ) {
312
+ // An empty SQL value has nothing to seed. Release the first real keystroke
313
+ // immediately, but when a fragment already exists defer the ambiguous
314
+ // reconcile decision for one task: active-peer or just-emitted local content
315
+ // is live and must be adopted; stale persisted CRDT with no active writer is
316
+ // cleared by the canonical SQL snapshot.
317
+ if (!value.trim()) {
312
318
  seededRef.current = true;
313
- return;
314
- }
319
+ const fragment = ydoc.getXmlFragment("default");
320
+ const currentMarkdown = getMarkdown(editor);
321
+ if (fragment.length === 0 && !currentMarkdown.trim()) return;
315
322
 
323
+ emptySnapshotDecisionPendingRef.current = true;
324
+ let cancelled = false;
325
+ const adoptTimer = setTimeout(
326
+ () => {
327
+ if (cancelled || editor.isDestroyed) return;
328
+ const projectedMarkdown = getMarkdown(editor);
329
+ const isOwnFreshEdit =
330
+ projectedMarkdown.trim() &&
331
+ (projectedMarkdown === lastEmittedRef.current ||
332
+ recentEmittedRef.current.includes(projectedMarkdown));
333
+ if (
334
+ projectedMarkdown.trim() &&
335
+ (peerCountRef.current > 0 || isOwnFreshEdit)
336
+ ) {
337
+ lastAppliedValueRef.current = value;
338
+ lastAppliedSerializedRef.current = projectedMarkdown;
339
+ if (contentUpdatedAt) {
340
+ lastAppliedUpdatedAtRef.current = contentUpdatedAt;
341
+ }
342
+ }
343
+ emptySnapshotDecisionPendingRef.current = false;
344
+ },
345
+ awareness ? PEER_SETTLE_MS : 0,
346
+ );
347
+ return () => {
348
+ cancelled = true;
349
+ clearTimeout(adoptTimer);
350
+ emptySnapshotDecisionPendingRef.current = false;
351
+ };
352
+ }
353
+ if (!isLeadClient) return;
316
354
  let cancelled = false;
317
355
  // Defer via a timer task (NOT a microtask — microtasks can still run
318
356
  // inside React's commit and trigger flushSync-from-lifecycle warnings).
357
+ // Read the editor and fragment INSIDE that task. The collaboration
358
+ // extension can finish projecting an already-populated Y.Doc into
359
+ // ProseMirror after this effect is scheduled. Capturing the pre-projection
360
+ // empty editor here would incorrectly seed the SQL snapshot alongside the
361
+ // existing CRDT content, duplicating the whole document after reload.
319
362
  const seedTimer = setTimeout(() => {
320
363
  if (cancelled || editor.isDestroyed) return;
364
+ const fragment = ydoc.getXmlFragment("default");
365
+ const currentMarkdown = getMarkdown(editor);
366
+ // Seed only when the shared doc is genuinely empty — either the fragment
367
+ // has no nodes yet, or it holds no semantic markdown (an empty paragraph,
368
+ // or an app's sentinel-empty filler via a custom `shouldSeed`).
369
+ if (
370
+ !shouldSeed({
371
+ value,
372
+ currentMarkdown,
373
+ fragmentLength: fragment.length,
374
+ })
375
+ ) {
376
+ seededRef.current = true;
377
+ return;
378
+ }
321
379
  isSettingContentRef.current = true;
322
380
  try {
323
381
  setContent(editor, value, {});
@@ -362,16 +420,26 @@ export function useCollabReconcile({
362
420
  // With peers present, a peer's edit also arrives via Yjs. Defer one poll
363
421
  // cycle (+margin) and re-check before applying via setContent so the same
364
422
  // change isn't inserted twice (Yjs + setContent → duplicated region).
365
- const PEER_SETTLE_MS = 2500;
366
-
367
423
  const apply = (deferred = false) => {
368
424
  if (cancelled || editor.isDestroyed) return;
369
425
  // In collab mode, defer all reconcile until the shared doc is seeded so we
370
426
  // never setContent over an unseeded fragment.
371
- if (collab && (!collabSynced || !seededRef.current)) {
427
+ if (collab && !collabSynced) {
372
428
  retry = setTimeout(() => apply(deferred), 300);
373
429
  return;
374
430
  }
431
+ // The seed decision itself is deferred one task so Collaboration can
432
+ // project persisted Y.Doc state before we inspect it. Poll that short
433
+ // handoff promptly: waiting the full provider cadence here makes a newer
434
+ // canonical SQL snapshot visibly stale after reload.
435
+ if (collab && !seededRef.current) {
436
+ retry = setTimeout(() => apply(deferred), 25);
437
+ return;
438
+ }
439
+ if (collab && emptySnapshotDecisionPendingRef.current) {
440
+ retry = setTimeout(() => apply(deferred), 50);
441
+ return;
442
+ }
375
443
  const currentMarkdown = getMarkdown(editor);
376
444
  // Compare against the canonical form the editor would emit so a serializer
377
445
  // that re-normalizes (Content's NFM) still recognizes "already in sync".
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -49,8 +49,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
49
49
  }>;
50
50
  /** DELETE /_agent-native/resources/:id — delete a resource */
51
51
  export declare function handleDeleteResource(event: any): Promise<{
52
- error: string;
53
52
  ok?: undefined;
53
+ error: string;
54
54
  } | {
55
55
  error?: undefined;
56
56
  ok: boolean;
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — re-run the validator against the
54
54
  * current stored value without changing anything. Useful for the "Test" button.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,12 +95,12 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
106
106
  //# sourceMappingURL=routes.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.120.3",
3
+ "version": "0.120.4",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -390,7 +390,7 @@
390
390
  "yjs": "^13.6.31",
391
391
  "zod": "^4.3.6",
392
392
  "@agent-native/recap-cli": "0.5.1",
393
- "@agent-native/toolkit": "^0.10.5"
393
+ "@agent-native/toolkit": "^0.10.6"
394
394
  },
395
395
  "devDependencies": {
396
396
  "@ai-sdk/anthropic": "^3.0.71",