@agent-native/core 0.161.10 → 0.161.14
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/analytics/actions/clear-selected-dashboard-object.ts +71 -0
- package/corpus/templates/analytics/actions/view-screen.ts +68 -4
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -66
- package/corpus/templates/analytics/app/lib/selected-object.ts +53 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +21 -0
- package/corpus/templates/analytics/changelog/2026-08-18-ask-no-stale-dashboard-context.md +6 -0
- package/corpus/templates/calendar/app/root.tsx +12 -4
- package/corpus/templates/calendar/changelog/2026-08-18-calendar-stays-responsive-in-the-desktop-app-without-focus-t.md +6 -0
- package/corpus/templates/clips/app/components/library/recording-card.tsx +19 -3
- package/corpus/templates/clips/changelog/2026-08-18-deleting-a-recording-no-longer-freezes-clips.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +9 -10
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +4 -3
- package/corpus/templates/clips/desktop/src/lib/recording-start-guard.ts +26 -0
- package/corpus/templates/content/actions/export-document.ts +68 -3
- package/corpus/templates/content/changelog/2026-08-06-database-exports-now-include-their-pages.md +6 -0
- package/corpus/templates/content/shared/document-export.ts +25 -0
- package/corpus/templates/design/.agents/skills/creative-context/SKILL.md +3 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +66 -0
- package/corpus/templates/design/app/pages/Index.tsx +45 -1
- package/corpus/templates/design/app/pages/design-editor/creative-context-precedent.ts +233 -0
- package/corpus/templates/design/changelog/2026-08-11-new-designs-now-skip-the-intake-questions-and-follow-your-ex.md +6 -0
- package/corpus/templates/dispatch/app/chat-first-pages.ts +40 -40
- package/corpus/templates/dispatch/app/i18n-data.ts +5 -5
- package/corpus/templates/factory/AGENTS.md +30 -29
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +56 -9
- package/dist/action.js +68 -2
- package/dist/agent/chat-health-alert.js +10 -1
- package/dist/client/AssistantChat.js +1 -0
- package/dist/client/chat-first/app-open-actions.js +1 -1
- package/dist/client/chat-first/copy.js +2 -2
- package/dist/client/conversation/use-near-bottom-autoscroll.js +34 -4
- package/dist/client/embed-auth.js +40 -3
- package/dist/client/mcp-apps/McpAppRenderer.js +7 -2
- package/dist/client/route-state.js +42 -5
- package/dist/client/use-change-version.js +71 -25
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +223 -45
- package/dist/collab/awareness.js +37 -4
- package/dist/collab/client.js +17 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/embed-app.js +71 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/poll.d.ts +22 -14
- package/dist/server/poll.js +243 -67
- package/dist/styles/agent-conversation.css +25 -4
- package/package.json +1 -1
package/dist/server/poll.js
CHANGED
|
@@ -69,6 +69,7 @@ const ACCESS_CACHE_DENY_TTL_MS = 5_000;
|
|
|
69
69
|
/** Max cache entries before FIFO eviction kicks in. */
|
|
70
70
|
const ACCESS_CACHE_MAX = 500;
|
|
71
71
|
const SCREEN_REFRESH_KEY = "__screen_refresh__";
|
|
72
|
+
const SCREEN_REFRESH_QUERY_LIMIT = 256;
|
|
72
73
|
/**
|
|
73
74
|
* DB-assigned version allocation (see `AppSyncStateOptions.dbAssignedVersions`).
|
|
74
75
|
* The allocator is a one-row table advanced with `GREATEST(v + 1, epoch_ms)` —
|
|
@@ -122,6 +123,32 @@ function sqlWatermarkValue(value) {
|
|
|
122
123
|
return value;
|
|
123
124
|
return undefined;
|
|
124
125
|
}
|
|
126
|
+
function compareSyncCursors(a, b) {
|
|
127
|
+
if (a.version !== b.version)
|
|
128
|
+
return a.version - b.version;
|
|
129
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
130
|
+
}
|
|
131
|
+
function cursorForEvent(event) {
|
|
132
|
+
return { version: event.version, id: event.cursorId ?? "" };
|
|
133
|
+
}
|
|
134
|
+
function encodeSyncCursor(cursor) {
|
|
135
|
+
return `${cursor.version}.${cursor.id}`;
|
|
136
|
+
}
|
|
137
|
+
function decodeSyncCursor(value) {
|
|
138
|
+
if (typeof value !== "string")
|
|
139
|
+
return undefined;
|
|
140
|
+
const separator = value.indexOf(".");
|
|
141
|
+
if (separator < 1)
|
|
142
|
+
return undefined;
|
|
143
|
+
const version = Number(value.slice(0, separator));
|
|
144
|
+
const id = value.slice(separator + 1);
|
|
145
|
+
if (!Number.isSafeInteger(version) || version < 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
return { version, id };
|
|
148
|
+
}
|
|
149
|
+
function isEventAfterCursor(event, cursor) {
|
|
150
|
+
return compareSyncCursors(cursorForEvent(event), cursor) > 0;
|
|
151
|
+
}
|
|
125
152
|
function syncEventsDisabled() {
|
|
126
153
|
return (process.env.AGENT_NATIVE_SYNC_EVENTS_DISABLE === "1" ||
|
|
127
154
|
(process.env.VITEST === "true" &&
|
|
@@ -260,11 +287,15 @@ export class AppSyncState {
|
|
|
260
287
|
// the same range (seeded from DB, then incremented via Date.now). Plain
|
|
261
288
|
// ++counter diverges across cold starts.
|
|
262
289
|
version = 0;
|
|
290
|
+
latestCursor = { version: 0, id: "" };
|
|
263
291
|
buffer = [];
|
|
264
292
|
pollEmitter = new EventEmitter();
|
|
265
293
|
syncEventsInitPromise;
|
|
266
|
-
|
|
294
|
+
// Cold starts should not immediately run a potentially large anti-join. A
|
|
295
|
+
// long-lived process will still prune on its first scheduled interval.
|
|
296
|
+
lastDurablePrune = Date.now();
|
|
267
297
|
durablePruneFailures = 0;
|
|
298
|
+
durableWriteFailures = 0;
|
|
268
299
|
/**
|
|
269
300
|
* Whether we've seeded `version` from the DB. In serverless (Netlify,
|
|
270
301
|
* Vercel, etc.) each invocation starts fresh — without seeding, `version`
|
|
@@ -295,8 +326,9 @@ export class AppSyncState {
|
|
|
295
326
|
* after a restart (where an existing row would look like a fresh bump).
|
|
296
327
|
*/
|
|
297
328
|
lastScreenRefreshTs = 0;
|
|
329
|
+
lastScreenRefreshSessionId = "";
|
|
298
330
|
screenRefreshInitialized = false;
|
|
299
|
-
|
|
331
|
+
screenRefreshHasMore = false;
|
|
300
332
|
localEmittersWired = false;
|
|
301
333
|
/**
|
|
302
334
|
* TTL'd access cache for the access-aware branch of `canSeeChangeForUser`,
|
|
@@ -460,8 +492,9 @@ export class AppSyncState {
|
|
|
460
492
|
* `sync_events_created_at_id_idx` drives the batch and the delete is a
|
|
461
493
|
* bounded index range scan.
|
|
462
494
|
*
|
|
463
|
-
* The `lastDurablePrune` throttle below is per-process
|
|
464
|
-
*
|
|
495
|
+
* The `lastDurablePrune` throttle below is per-process and starts at process
|
|
496
|
+
* initialization, so a cold serverless worker does not run a potentially
|
|
497
|
+
* large delete during its first request. Postgres workers also take a
|
|
465
498
|
* transaction-scoped advisory lease for each batch. The lease and delete
|
|
466
499
|
* are one bounded statement, so a serverless worker killed after the
|
|
467
500
|
* statement starts cannot leave an open transaction.
|
|
@@ -521,18 +554,23 @@ export class AppSyncState {
|
|
|
521
554
|
}
|
|
522
555
|
}
|
|
523
556
|
}
|
|
557
|
+
reportDurableWriteFailure(error, event) {
|
|
558
|
+
this.durableWriteFailures++;
|
|
559
|
+
if (this.durableWriteFailures === 1) {
|
|
560
|
+
console.warn(`[agent-native] sync_events write failed for ${event.source}/${event.type}${event.key ? `/${event.key}` : ""}; durable replay is unavailable until the database recovers:`, error instanceof Error ? error.message : String(error));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
524
563
|
async persistSyncEvent(event, dedupeKey, presetId) {
|
|
525
564
|
if (!(await this.ensureSyncEventsTable()))
|
|
526
565
|
return;
|
|
527
566
|
const client = this.getDb();
|
|
528
|
-
await client
|
|
529
|
-
.execute({
|
|
567
|
+
await client.execute({
|
|
530
568
|
sql: this.isPg()
|
|
531
569
|
? `INSERT INTO sync_events (id, version, event_json, source, type, event_key, owner, org_id, resource_type, resource_id, created_at)
|
|
532
|
-
|
|
533
|
-
|
|
570
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
571
|
+
ON CONFLICT (id) DO NOTHING`
|
|
534
572
|
: `INSERT OR IGNORE INTO sync_events (id, version, event_json, source, type, event_key, owner, org_id, resource_type, resource_id, created_at)
|
|
535
|
-
|
|
573
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
536
574
|
args: [
|
|
537
575
|
// presetId lets a gated fallback reuse the allocating attempt's id,
|
|
538
576
|
// so a commit-then-timeout can't produce the same event twice.
|
|
@@ -548,8 +586,8 @@ export class AppSyncState {
|
|
|
548
586
|
event.resourceId ?? null,
|
|
549
587
|
Date.now(),
|
|
550
588
|
],
|
|
551
|
-
})
|
|
552
|
-
|
|
589
|
+
});
|
|
590
|
+
this.durableWriteFailures = 0;
|
|
553
591
|
await this.pruneDurableEvents(client);
|
|
554
592
|
}
|
|
555
593
|
/**
|
|
@@ -797,17 +835,27 @@ export class AppSyncState {
|
|
|
797
835
|
// drop every later event.
|
|
798
836
|
this.recordChain = this.recordChain
|
|
799
837
|
.then(() => this.recordWithDbVersion(event, opts?.dedupeKey))
|
|
800
|
-
.catch(() => {
|
|
838
|
+
.catch((error) => {
|
|
839
|
+
this.reportDurableWriteFailure(error, event);
|
|
840
|
+
});
|
|
801
841
|
return;
|
|
802
842
|
}
|
|
803
843
|
this.version = Math.max(this.version + 1, Date.now());
|
|
804
|
-
const
|
|
844
|
+
const provisional = { ...event, version: this.version };
|
|
845
|
+
const cursorId = this.durableEventId(provisional, opts?.dedupeKey);
|
|
846
|
+
const entry = { ...provisional, cursorId };
|
|
805
847
|
this.commitEntry(entry);
|
|
806
|
-
void this.persistSyncEvent(entry, opts?.dedupeKey)
|
|
848
|
+
void this.persistSyncEvent(entry, opts?.dedupeKey, cursorId).catch((error) => {
|
|
849
|
+
this.reportDurableWriteFailure(error, entry);
|
|
850
|
+
});
|
|
807
851
|
}
|
|
808
852
|
/** Buffer + emit. Shared by both version-allocation modes. */
|
|
809
853
|
commitEntry(entry) {
|
|
810
854
|
this.buffer.push(entry);
|
|
855
|
+
const cursor = cursorForEvent(entry);
|
|
856
|
+
if (compareSyncCursors(cursor, this.latestCursor) > 0) {
|
|
857
|
+
this.latestCursor = cursor;
|
|
858
|
+
}
|
|
811
859
|
if (this.buffer.length > MAX_BUFFER) {
|
|
812
860
|
this.buffer.splice(0, this.buffer.length - MAX_BUFFER);
|
|
813
861
|
}
|
|
@@ -826,7 +874,7 @@ export class AppSyncState {
|
|
|
826
874
|
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
827
875
|
let version = null;
|
|
828
876
|
try {
|
|
829
|
-
version = await this.persistWithDbAssignedVersion(event, id);
|
|
877
|
+
version = await this.persistWithDbAssignedVersion({ ...event, cursorId: id }, id);
|
|
830
878
|
}
|
|
831
879
|
catch {
|
|
832
880
|
version = null;
|
|
@@ -851,10 +899,16 @@ export class AppSyncState {
|
|
|
851
899
|
console.warn("[agent-native] sync version allocation failed; falling back to clock-assigned versions");
|
|
852
900
|
}
|
|
853
901
|
this.version = Math.max(this.version + 1, Date.now());
|
|
854
|
-
const entry = {
|
|
902
|
+
const entry = {
|
|
903
|
+
...event,
|
|
904
|
+
version: this.version,
|
|
905
|
+
cursorId: id,
|
|
906
|
+
};
|
|
855
907
|
await this.alignVersionAllocator(this.version);
|
|
856
908
|
this.commitEntryForChain(entry);
|
|
857
|
-
void this.persistSyncEvent(entry, dedupeKey, id)
|
|
909
|
+
void this.persistSyncEvent(entry, dedupeKey, id).catch((error) => {
|
|
910
|
+
this.reportDurableWriteFailure(error, entry);
|
|
911
|
+
});
|
|
858
912
|
return;
|
|
859
913
|
}
|
|
860
914
|
// A deterministic-id dedupe loser adopts the winner's (possibly older)
|
|
@@ -862,7 +916,7 @@ export class AppSyncState {
|
|
|
862
916
|
// delivered correctly and the version-keyed combined-read dedupe collapses
|
|
863
917
|
// it against the durable row.
|
|
864
918
|
this.version = Math.max(this.version, version);
|
|
865
|
-
this.commitEntryForChain({ ...event, version });
|
|
919
|
+
this.commitEntryForChain({ ...event, version, cursorId: id });
|
|
866
920
|
}
|
|
867
921
|
/**
|
|
868
922
|
* `commitEntry` for the deferred (chained) path: the buffer push must land
|
|
@@ -929,32 +983,61 @@ export class AppSyncState {
|
|
|
929
983
|
* Get changes after a given version, filtered to events the caller is
|
|
930
984
|
* allowed to see.
|
|
931
985
|
*/
|
|
932
|
-
getChangesSinceForUser(since, userEmail, orgId) {
|
|
933
|
-
|
|
934
|
-
|
|
986
|
+
getChangesSinceForUser(since, userEmail, orgId, cursor) {
|
|
987
|
+
const readCursor = cursor ?? { version: since, id: "" };
|
|
988
|
+
if ((!cursor && since >= this.version) ||
|
|
989
|
+
(cursor && compareSyncCursors(this.latestCursor, readCursor) <= 0)) {
|
|
990
|
+
return {
|
|
991
|
+
version: this.version,
|
|
992
|
+
events: [],
|
|
993
|
+
...(cursor ? { cursor: encodeSyncCursor(readCursor) } : {}),
|
|
994
|
+
};
|
|
935
995
|
}
|
|
936
996
|
const events = [];
|
|
937
997
|
let version = this.version;
|
|
998
|
+
let lastCursor = readCursor;
|
|
938
999
|
for (const event of this.buffer) {
|
|
939
|
-
if (event.version <= since)
|
|
1000
|
+
if (cursor ? !isEventAfterCursor(event, readCursor) : event.version <= since) {
|
|
940
1001
|
continue;
|
|
1002
|
+
}
|
|
941
1003
|
const visibility = this.getChangeVisibilityForUser(event, userEmail, orgId);
|
|
942
1004
|
if (visibility === "visible") {
|
|
943
1005
|
events.push(event);
|
|
1006
|
+
lastCursor = cursorForEvent(event);
|
|
944
1007
|
continue;
|
|
945
1008
|
}
|
|
946
1009
|
if (visibility === "pending") {
|
|
947
1010
|
version = Math.max(since, event.version - 1);
|
|
948
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
version,
|
|
1013
|
+
events,
|
|
1014
|
+
cursorLimited: true,
|
|
1015
|
+
...(cursor && lastCursor !== readCursor
|
|
1016
|
+
? { cursor: encodeSyncCursor(lastCursor) }
|
|
1017
|
+
: {}),
|
|
1018
|
+
};
|
|
949
1019
|
}
|
|
1020
|
+
lastCursor = cursorForEvent(event);
|
|
950
1021
|
}
|
|
951
|
-
return {
|
|
1022
|
+
return {
|
|
1023
|
+
version,
|
|
1024
|
+
events,
|
|
1025
|
+
...(cursor && compareSyncCursors(lastCursor, readCursor) > 0
|
|
1026
|
+
? { cursor: encodeSyncCursor(lastCursor) }
|
|
1027
|
+
: {}),
|
|
1028
|
+
};
|
|
952
1029
|
}
|
|
953
|
-
async getDurableChangesSinceForUser(since, userEmail, orgId) {
|
|
954
|
-
if (since <= 0 ||
|
|
1030
|
+
async getDurableChangesSinceForUser(since, userEmail, orgId, cursor) {
|
|
1031
|
+
if ((!cursor && since <= 0) ||
|
|
1032
|
+
(cursor && cursor.version <= 0 && cursor.id === "") ||
|
|
1033
|
+
!(await this.ensureSyncEventsTable())) {
|
|
955
1034
|
return { version: this.version, events: [] };
|
|
956
1035
|
}
|
|
957
1036
|
try {
|
|
1037
|
+
const readCursor = cursor ?? { version: since, id: "" };
|
|
1038
|
+
const compositeCursorSql = cursor
|
|
1039
|
+
? "(version > ? OR (version = ? AND id > ?))"
|
|
1040
|
+
: "version > ?";
|
|
958
1041
|
// Scope the fetch to rows that could ever be visible to this caller
|
|
959
1042
|
// before paying to JSON.parse and visibility-check every deployment-wide
|
|
960
1043
|
// event: deployment-global rows (no owner, no org), the caller's own
|
|
@@ -965,19 +1048,29 @@ export class AppSyncState {
|
|
|
965
1048
|
// `orgId` bind param, which makes `org_id = ?` match no row in both
|
|
966
1049
|
// dialects — mirroring the `event.orgId && orgId` truthy check.
|
|
967
1050
|
const result = await this.getDb().execute({
|
|
968
|
-
sql: `SELECT version, event_json FROM sync_events WHERE
|
|
1051
|
+
sql: `SELECT id, version, event_json FROM sync_events WHERE ${compositeCursorSql}
|
|
969
1052
|
AND (
|
|
970
1053
|
(owner IS NULL AND org_id IS NULL)
|
|
971
1054
|
OR owner = ?
|
|
972
1055
|
OR org_id = ?
|
|
973
1056
|
OR resource_type IS NOT NULL
|
|
974
1057
|
)
|
|
975
|
-
ORDER BY version ASC LIMIT ?`,
|
|
976
|
-
args:
|
|
1058
|
+
ORDER BY version ASC, id ASC LIMIT ?`,
|
|
1059
|
+
args: cursor
|
|
1060
|
+
? [
|
|
1061
|
+
cursor.version,
|
|
1062
|
+
cursor.version,
|
|
1063
|
+
cursor.id,
|
|
1064
|
+
userEmail,
|
|
1065
|
+
orgId ?? null,
|
|
1066
|
+
DURABLE_READ_LIMIT + 1,
|
|
1067
|
+
]
|
|
1068
|
+
: [since, userEmail, orgId ?? null, DURABLE_READ_LIMIT + 1],
|
|
977
1069
|
});
|
|
978
1070
|
const events = [];
|
|
979
1071
|
let version = Math.max(this.version, since);
|
|
980
1072
|
let lastDurableVersion = since;
|
|
1073
|
+
let lastCursor = readCursor;
|
|
981
1074
|
const rows = result.rows.slice(0, DURABLE_READ_LIMIT);
|
|
982
1075
|
const overflowVersion = timestampValue(result.rows[DURABLE_READ_LIMIT]?.version);
|
|
983
1076
|
for (const row of rows) {
|
|
@@ -986,6 +1079,8 @@ export class AppSyncState {
|
|
|
986
1079
|
lastDurableVersion = rawVersion;
|
|
987
1080
|
if (rawVersion > version)
|
|
988
1081
|
version = rawVersion;
|
|
1082
|
+
const rowId = typeof row.id === "string" ? row.id : "";
|
|
1083
|
+
let rowCursor = rowId ? { version: rawVersion, id: rowId } : undefined;
|
|
989
1084
|
let event = null;
|
|
990
1085
|
try {
|
|
991
1086
|
const parsed = JSON.parse(String(row.event_json));
|
|
@@ -996,17 +1091,33 @@ export class AppSyncState {
|
|
|
996
1091
|
event = {
|
|
997
1092
|
...parsed,
|
|
998
1093
|
version: rawVersion || parsed.version,
|
|
1094
|
+
...(rowId || typeof parsed.cursorId === "string"
|
|
1095
|
+
? {
|
|
1096
|
+
cursorId: rowId || parsed.cursorId,
|
|
1097
|
+
}
|
|
1098
|
+
: {}),
|
|
999
1099
|
};
|
|
1000
1100
|
}
|
|
1001
1101
|
}
|
|
1002
1102
|
catch {
|
|
1003
1103
|
event = null;
|
|
1004
1104
|
}
|
|
1005
|
-
if (!event)
|
|
1105
|
+
if (!event) {
|
|
1106
|
+
// Malformed durable rows are skipped from delivery, but their
|
|
1107
|
+
// composite cursor still needs to advance. Otherwise every poll
|
|
1108
|
+
// rereads the same bad row forever and can never reach later rows.
|
|
1109
|
+
if (rowCursor)
|
|
1110
|
+
lastCursor = rowCursor;
|
|
1006
1111
|
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (!rowCursor && typeof event.cursorId === "string") {
|
|
1114
|
+
rowCursor = { version: event.version, id: event.cursorId };
|
|
1115
|
+
}
|
|
1007
1116
|
const visibility = this.getChangeVisibilityForUser(event, userEmail, orgId);
|
|
1008
1117
|
if (visibility === "visible") {
|
|
1009
1118
|
events.push(event);
|
|
1119
|
+
if (rowCursor)
|
|
1120
|
+
lastCursor = rowCursor;
|
|
1010
1121
|
continue;
|
|
1011
1122
|
}
|
|
1012
1123
|
if (visibility === "pending") {
|
|
@@ -1014,10 +1125,23 @@ export class AppSyncState {
|
|
|
1014
1125
|
version: Math.max(since, event.version - 1),
|
|
1015
1126
|
events,
|
|
1016
1127
|
cursorLimited: true,
|
|
1128
|
+
...(cursor && compareSyncCursors(lastCursor, readCursor) > 0
|
|
1129
|
+
? { cursor: encodeSyncCursor(lastCursor) }
|
|
1130
|
+
: {}),
|
|
1017
1131
|
};
|
|
1018
1132
|
}
|
|
1133
|
+
if (rowCursor)
|
|
1134
|
+
lastCursor = rowCursor;
|
|
1019
1135
|
}
|
|
1020
1136
|
if (rows.length >= DURABLE_READ_LIMIT) {
|
|
1137
|
+
if (cursor) {
|
|
1138
|
+
return {
|
|
1139
|
+
version: Math.max(since, lastDurableVersion),
|
|
1140
|
+
events,
|
|
1141
|
+
cursor: encodeSyncCursor(lastCursor),
|
|
1142
|
+
cursorLimited: result.rows.length > DURABLE_READ_LIMIT,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1021
1145
|
if (overflowVersion === lastDurableVersion) {
|
|
1022
1146
|
const boundaryVersion = lastDurableVersion;
|
|
1023
1147
|
return {
|
|
@@ -1032,20 +1156,31 @@ export class AppSyncState {
|
|
|
1032
1156
|
cursorLimited: true,
|
|
1033
1157
|
};
|
|
1034
1158
|
}
|
|
1035
|
-
return {
|
|
1159
|
+
return {
|
|
1160
|
+
version,
|
|
1161
|
+
events,
|
|
1162
|
+
...(cursor && compareSyncCursors(lastCursor, readCursor) > 0
|
|
1163
|
+
? { cursor: encodeSyncCursor(lastCursor) }
|
|
1164
|
+
: {}),
|
|
1165
|
+
};
|
|
1036
1166
|
}
|
|
1037
1167
|
catch {
|
|
1038
|
-
return {
|
|
1168
|
+
return {
|
|
1169
|
+
version: this.version,
|
|
1170
|
+
events: [],
|
|
1171
|
+
...(cursor ? { cursor: encodeSyncCursor(cursor) } : {}),
|
|
1172
|
+
};
|
|
1039
1173
|
}
|
|
1040
1174
|
}
|
|
1041
|
-
async getCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents) {
|
|
1042
|
-
const memory = this.getChangesSinceForUser(since, userEmail, orgId);
|
|
1175
|
+
async getCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor) {
|
|
1176
|
+
const memory = this.getChangesSinceForUser(since, userEmail, orgId, cursor);
|
|
1043
1177
|
if (!useDurableEvents)
|
|
1044
1178
|
return memory;
|
|
1045
|
-
const durable = await this.getDurableChangesSinceForUser(since, userEmail, orgId);
|
|
1179
|
+
const durable = await this.getDurableChangesSinceForUser(since, userEmail, orgId, cursor);
|
|
1046
1180
|
const byIdentity = new Map();
|
|
1047
1181
|
for (const event of [...durable.events, ...memory.events]) {
|
|
1048
1182
|
byIdentity.set(JSON.stringify([
|
|
1183
|
+
event.cursorId,
|
|
1049
1184
|
event.version,
|
|
1050
1185
|
event.source,
|
|
1051
1186
|
event.type,
|
|
@@ -1056,7 +1191,37 @@ export class AppSyncState {
|
|
|
1056
1191
|
event.resourceId,
|
|
1057
1192
|
]), event);
|
|
1058
1193
|
}
|
|
1059
|
-
const events = Array.from(byIdentity.values()).sort((a, b) => a
|
|
1194
|
+
const events = Array.from(byIdentity.values()).sort((a, b) => compareSyncCursors(cursorForEvent(a), cursorForEvent(b)));
|
|
1195
|
+
if (cursor) {
|
|
1196
|
+
const limitedCursors = [memory, durable]
|
|
1197
|
+
.filter((result) => result.cursorLimited)
|
|
1198
|
+
.map((result) => decodeSyncCursor(result.cursor))
|
|
1199
|
+
.filter((value) => !!value);
|
|
1200
|
+
if (limitedCursors.length > 0) {
|
|
1201
|
+
const boundary = limitedCursors.reduce((minimum, value) => compareSyncCursors(value, minimum) < 0 ? value : minimum);
|
|
1202
|
+
return {
|
|
1203
|
+
version: boundary.version,
|
|
1204
|
+
cursor: encodeSyncCursor(boundary),
|
|
1205
|
+
cursorLimited: true,
|
|
1206
|
+
events: events.filter((event) => compareSyncCursors(cursorForEvent(event), boundary) <= 0),
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
const responseCursor = [
|
|
1210
|
+
cursor,
|
|
1211
|
+
decodeSyncCursor(memory.cursor),
|
|
1212
|
+
decodeSyncCursor(durable.cursor),
|
|
1213
|
+
...events.map(cursorForEvent),
|
|
1214
|
+
]
|
|
1215
|
+
.filter((value) => !!value)
|
|
1216
|
+
.reduce((maximum, value) => compareSyncCursors(value, maximum) > 0 ? value : maximum, cursor);
|
|
1217
|
+
return {
|
|
1218
|
+
version: Math.max(memory.version, durable.version, since),
|
|
1219
|
+
events,
|
|
1220
|
+
...(compareSyncCursors(responseCursor, cursor) > 0
|
|
1221
|
+
? { cursor: encodeSyncCursor(responseCursor) }
|
|
1222
|
+
: {}),
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1060
1225
|
const limitedVersions = [memory, durable]
|
|
1061
1226
|
.filter((result) => result.cursorLimited)
|
|
1062
1227
|
.map((result) => result.version);
|
|
@@ -1088,7 +1253,7 @@ export class AppSyncState {
|
|
|
1088
1253
|
readActionMarkerMaxUpdatedAt(db),
|
|
1089
1254
|
db
|
|
1090
1255
|
.execute({
|
|
1091
|
-
sql: "SELECT session_id, updated_at FROM application_state WHERE key = ?",
|
|
1256
|
+
sql: "SELECT session_id, updated_at FROM application_state WHERE key = ? ORDER BY updated_at DESC, session_id DESC LIMIT 1",
|
|
1092
1257
|
args: [SCREEN_REFRESH_KEY],
|
|
1093
1258
|
})
|
|
1094
1259
|
.catch(() => ({ rows: [] })),
|
|
@@ -1130,12 +1295,11 @@ export class AppSyncState {
|
|
|
1130
1295
|
this.lastActionMarkerTs = Math.max(0, actionMarkerTs - ACTION_MARKER_REPLAY_WINDOW_MS);
|
|
1131
1296
|
this.replayActionMarkerOnce = actionMarkerTs > 0;
|
|
1132
1297
|
this.lastScreenRefreshTs = refreshTs;
|
|
1133
|
-
this.
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1298
|
+
this.lastScreenRefreshSessionId =
|
|
1299
|
+
typeof refreshResult.rows[0]?.session_id === "string"
|
|
1300
|
+
? refreshResult.rows[0].session_id
|
|
1301
|
+
: "";
|
|
1302
|
+
this.screenRefreshHasMore = false;
|
|
1139
1303
|
this.screenRefreshInitialized = true;
|
|
1140
1304
|
// Skip the redundant cold-start recheck unless there is an existing durable
|
|
1141
1305
|
// action marker that the first poll still needs to emit.
|
|
@@ -1207,15 +1371,28 @@ export class AppSyncState {
|
|
|
1207
1371
|
const replayActionMarker = this.replayActionMarkerOnce;
|
|
1208
1372
|
this.replayActionMarkerOnce = false;
|
|
1209
1373
|
const appStateChanged = appStateMaxTs > this.lastAppStateTs || replayActionMarker;
|
|
1210
|
-
const
|
|
1374
|
+
const screenRefreshNeedsCheck = appStateChanged || this.screenRefreshHasMore;
|
|
1375
|
+
const [appResult, refreshResult, extensionMarkerTs] = screenRefreshNeedsCheck
|
|
1211
1376
|
? await Promise.all([
|
|
1377
|
+
appStateChanged
|
|
1378
|
+
? db.execute({
|
|
1379
|
+
sql: "SELECT session_id, key, updated_at FROM application_state WHERE updated_at > ? ORDER BY updated_at ASC",
|
|
1380
|
+
args: [this.lastAppStateTs],
|
|
1381
|
+
})
|
|
1382
|
+
: Promise.resolve({ rows: [] }),
|
|
1212
1383
|
db.execute({
|
|
1213
|
-
sql:
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
args: [
|
|
1384
|
+
sql: `SELECT session_id, updated_at, value FROM application_state
|
|
1385
|
+
WHERE key = ?
|
|
1386
|
+
AND (updated_at > ? OR (updated_at = ? AND session_id > ?))
|
|
1387
|
+
ORDER BY updated_at ASC, session_id ASC
|
|
1388
|
+
LIMIT ?`,
|
|
1389
|
+
args: [
|
|
1390
|
+
SCREEN_REFRESH_KEY,
|
|
1391
|
+
this.lastScreenRefreshTs,
|
|
1392
|
+
this.lastScreenRefreshTs,
|
|
1393
|
+
this.lastScreenRefreshSessionId,
|
|
1394
|
+
SCREEN_REFRESH_QUERY_LIMIT,
|
|
1395
|
+
],
|
|
1219
1396
|
}),
|
|
1220
1397
|
readExtensionMarkerMaxUpdatedAt(db),
|
|
1221
1398
|
])
|
|
@@ -1282,17 +1459,16 @@ export class AppSyncState {
|
|
|
1282
1459
|
// application_state row moved, which is also exactly when this block
|
|
1283
1460
|
// could not emit anything.
|
|
1284
1461
|
if (refreshResult) {
|
|
1285
|
-
const refreshTs = refreshResult.rows.reduce((max, row) => Math.max(max, timestampValue(row.updated_at)), 0);
|
|
1286
1462
|
if (!this.screenRefreshInitialized) {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1463
|
+
const lastRow = refreshResult.rows.at(-1);
|
|
1464
|
+
this.lastScreenRefreshTs = timestampValue(lastRow?.updated_at);
|
|
1465
|
+
this.lastScreenRefreshSessionId =
|
|
1466
|
+
typeof lastRow?.session_id === "string" ? lastRow.session_id : "";
|
|
1467
|
+
this.screenRefreshHasMore =
|
|
1468
|
+
refreshResult.rows.length >= SCREEN_REFRESH_QUERY_LIMIT;
|
|
1293
1469
|
this.screenRefreshInitialized = true;
|
|
1294
1470
|
}
|
|
1295
|
-
else
|
|
1471
|
+
else {
|
|
1296
1472
|
// Emit a per-user event only for the session(s) whose row actually
|
|
1297
1473
|
// advanced, scoped with `owner` so canSeeChangeForUser delivers it only
|
|
1298
1474
|
// to that user — not every authenticated poller.
|
|
@@ -1301,9 +1477,6 @@ export class AppSyncState {
|
|
|
1301
1477
|
if (!owner)
|
|
1302
1478
|
continue;
|
|
1303
1479
|
const rowTs = timestampValue(row.updated_at);
|
|
1304
|
-
if (rowTs <= (this.lastScreenRefreshTsBySession.get(owner) ?? 0)) {
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1307
1480
|
let scope;
|
|
1308
1481
|
try {
|
|
1309
1482
|
const raw = row.value;
|
|
@@ -1320,10 +1493,12 @@ export class AppSyncState {
|
|
|
1320
1493
|
key: SCREEN_REFRESH_KEY,
|
|
1321
1494
|
owner,
|
|
1322
1495
|
...(scope ? { scope } : {}),
|
|
1323
|
-
}, { dedupeKey: `screen-refresh|${rowTs}` });
|
|
1324
|
-
this.
|
|
1496
|
+
}, { dedupeKey: `screen-refresh|${rowTs}|${owner}` });
|
|
1497
|
+
this.lastScreenRefreshTs = rowTs;
|
|
1498
|
+
this.lastScreenRefreshSessionId = owner;
|
|
1325
1499
|
}
|
|
1326
|
-
this.
|
|
1500
|
+
this.screenRefreshHasMore =
|
|
1501
|
+
refreshResult.rows.length >= SCREEN_REFRESH_QUERY_LIMIT;
|
|
1327
1502
|
}
|
|
1328
1503
|
}
|
|
1329
1504
|
// Extension mutations write a durable marker row so delete and hide/unhide
|
|
@@ -1452,7 +1627,7 @@ export function getChangesSinceForUser(since, userEmail, orgId) {
|
|
|
1452
1627
|
/**
|
|
1453
1628
|
* Create an H3 handler for the poll endpoint.
|
|
1454
1629
|
*
|
|
1455
|
-
* GET /_agent-native/poll?since=N → { version, events[] }
|
|
1630
|
+
* GET /_agent-native/poll?since=N[&cursor=N.id] → { version, events[] }
|
|
1456
1631
|
*
|
|
1457
1632
|
* Requires an authenticated session. Events are filtered to the caller's
|
|
1458
1633
|
* tenant — global events (owner-less, table-level pings) reach every
|
|
@@ -1479,7 +1654,8 @@ export function createPollHandler(state = getDefaultAppSyncState()) {
|
|
|
1479
1654
|
// older processes that have not started writing durable events yet.
|
|
1480
1655
|
await state.checkExternalDbChanges({ durableEvents });
|
|
1481
1656
|
const query = getQuery(event);
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1657
|
+
const cursor = decodeSyncCursor(query.cursor);
|
|
1658
|
+
const since = cursor?.version ?? (parseInt(String(query.since ?? "0"), 10) || 0);
|
|
1659
|
+
return state.getCombinedChangesSinceForUser(since, session.email, session.orgId, durableEvents, cursor);
|
|
1484
1660
|
});
|
|
1485
1661
|
}
|
|
@@ -331,6 +331,27 @@
|
|
|
331
331
|
font-size: 12px;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
.agent-mcp-app__error {
|
|
335
|
+
pointer-events: none;
|
|
336
|
+
background: transparent;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.agent-mcp-app__error-box {
|
|
340
|
+
display: flex;
|
|
341
|
+
max-width: min(100%, 540px);
|
|
342
|
+
flex-direction: column;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 8px;
|
|
345
|
+
padding: 12px 14px;
|
|
346
|
+
border: 1px solid hsl(var(--destructive) / 0.35);
|
|
347
|
+
border-radius: calc(var(--radius) - 2px);
|
|
348
|
+
background: hsl(var(--background) / 0.96);
|
|
349
|
+
box-shadow: 0 8px 24px hsl(var(--foreground) / 0.12);
|
|
350
|
+
color: hsl(var(--destructive));
|
|
351
|
+
text-align: center;
|
|
352
|
+
pointer-events: auto;
|
|
353
|
+
}
|
|
354
|
+
|
|
334
355
|
.agent-mcp-app__error {
|
|
335
356
|
flex-direction: column;
|
|
336
357
|
text-align: center;
|
|
@@ -338,10 +359,10 @@
|
|
|
338
359
|
|
|
339
360
|
.agent-mcp-app__open {
|
|
340
361
|
min-height: 28px;
|
|
341
|
-
border: 1px solid hsl(var(--
|
|
362
|
+
border: 1px solid hsl(var(--foreground));
|
|
342
363
|
border-radius: calc(var(--radius) - 2px);
|
|
343
|
-
background: hsl(var(--
|
|
344
|
-
color: hsl(var(--
|
|
364
|
+
background: hsl(var(--foreground));
|
|
365
|
+
color: hsl(var(--background));
|
|
345
366
|
cursor: pointer;
|
|
346
367
|
font: inherit;
|
|
347
368
|
font-weight: 600;
|
|
@@ -349,7 +370,7 @@
|
|
|
349
370
|
}
|
|
350
371
|
|
|
351
372
|
.agent-mcp-app__open:hover {
|
|
352
|
-
|
|
373
|
+
opacity: 0.86;
|
|
353
374
|
}
|
|
354
375
|
|
|
355
376
|
.agent-mcp-app__error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.161.
|
|
3
|
+
"version": "0.161.14",
|
|
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": {
|