@agent-native/core 0.0.0-beta-20260819204836 → 0.0.0-beta-20260819215811

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 CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8473
34
+ - template files: 8474
@@ -127,6 +127,8 @@ import {
127
127
  buildDocumentTree,
128
128
  filterDocumentTreeDocuments,
129
129
  documentQueryFilter,
130
+ rollbackOptimisticCreatedDocument,
131
+ restoreDeletedDocumentSnapshots,
130
132
  } from "@/hooks/use-documents";
131
133
  import { useLocalStorage } from "@/hooks/use-local-storage";
132
134
  import {
@@ -1153,12 +1155,16 @@ export function DocumentSidebar({
1153
1155
  createdAt: now,
1154
1156
  updatedAt: now,
1155
1157
  });
1158
+ const previousDocuments = queryClient.getQueryData(
1159
+ LIST_DOCUMENTS_QUERY_KEY,
1160
+ );
1161
+ const previousPath = `${location.pathname}${location.search}${location.hash}`;
1156
1162
 
1157
1163
  // Optimistically inject into caches so UI updates immediately
1158
1164
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
1159
1165
  const docs: Document[] =
1160
1166
  old?.documents ?? (Array.isArray(old) ? old : []);
1161
- return { documents: [...docs, tempDoc] };
1167
+ return withDocumentsCacheShape(old, [...docs, tempDoc]);
1162
1168
  });
1163
1169
  queryClient.setQueryData(["action", "get-document", { id }], tempDoc);
1164
1170
  if (rootFilesDatabaseId) {
@@ -1207,7 +1213,11 @@ export function DocumentSidebar({
1207
1213
  });
1208
1214
  }
1209
1215
  } catch (err) {
1210
- // Revert optimistic updates
1216
+ rollbackOptimisticCreatedDocument(
1217
+ queryClient,
1218
+ id,
1219
+ previousDocuments !== undefined,
1220
+ );
1211
1221
  queryClient.invalidateQueries({
1212
1222
  queryKey: ["action", "list-documents"],
1213
1223
  });
@@ -1218,7 +1228,10 @@ export function DocumentSidebar({
1218
1228
  (current) => removeOptimisticItemFromContentDatabase(current, id),
1219
1229
  );
1220
1230
  }
1221
- navigate("/");
1231
+ navigate(previousPath, {
1232
+ replace: true,
1233
+ flushSync: true,
1234
+ });
1222
1235
  toast.error(t("sidebar.failedCreatePage"), {
1223
1236
  description:
1224
1237
  err instanceof Error ? err.message : t("empty.genericError"),
@@ -1228,6 +1241,9 @@ export function DocumentSidebar({
1228
1241
  [
1229
1242
  createDocument,
1230
1243
  localFileMode,
1244
+ location.hash,
1245
+ location.pathname,
1246
+ location.search,
1231
1247
  navigate,
1232
1248
  navigateToDocument,
1233
1249
  onNavigate,
@@ -1284,6 +1300,13 @@ export function DocumentSidebar({
1284
1300
  navigationCandidates.find((doc) => doc.isFavorite) ??
1285
1301
  [...navigationCandidates].sort(compareDocumentsByPosition)[0] ??
1286
1302
  null;
1303
+ const previousDocuments = queryClient.getQueryData(
1304
+ LIST_DOCUMENTS_QUERY_KEY,
1305
+ );
1306
+ const previousDocumentQueries = [...deletedIds].flatMap((deletedId) =>
1307
+ queryClient.getQueriesData(documentQueryFilter(deletedId)),
1308
+ );
1309
+ const previousPath = `${location.pathname}${location.search}${location.hash}`;
1287
1310
 
1288
1311
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: unknown) => {
1289
1312
  const cachedDocs: Document[] =
@@ -1317,11 +1340,17 @@ export function DocumentSidebar({
1317
1340
  queryKey: ["action", "list-documents"],
1318
1341
  });
1319
1342
  } catch (err) {
1343
+ restoreDeletedDocumentSnapshots(
1344
+ queryClient,
1345
+ previousDocuments,
1346
+ previousDocumentQueries,
1347
+ deletedIds,
1348
+ );
1320
1349
  queryClient.invalidateQueries({
1321
1350
  queryKey: ["action", "list-documents"],
1322
1351
  });
1323
- if (activeDeleted && activeDocumentId) {
1324
- navigate(`/page/${activeDocumentId}`, {
1352
+ if (activeDeleted) {
1353
+ navigate(previousPath, {
1325
1354
  replace: true,
1326
1355
  flushSync: true,
1327
1356
  });
@@ -1338,6 +1367,9 @@ export function DocumentSidebar({
1338
1367
  deleteDocument,
1339
1368
  documents,
1340
1369
  localFileMode,
1370
+ location.hash,
1371
+ location.pathname,
1372
+ location.search,
1341
1373
  navigate,
1342
1374
  queryClient,
1343
1375
  ],
@@ -1,7 +1,7 @@
1
1
  import type { Document } from "@shared/api";
2
2
  import { useQueryClient } from "@tanstack/react-query";
3
3
  import { useCallback } from "react";
4
- import { useNavigate } from "react-router";
4
+ import { useLocation, useNavigate } from "react-router";
5
5
  import { toast } from "sonner";
6
6
 
7
7
  import {
@@ -10,7 +10,10 @@ import {
10
10
  SELECTED_CONTENT_SPACE_STORAGE_KEY,
11
11
  } from "@/components/sidebar/select-content-space";
12
12
  import { useContentSpaces } from "@/hooks/use-content-spaces";
13
- import { useCreateDocument } from "@/hooks/use-documents";
13
+ import {
14
+ rollbackOptimisticCreatedDocument,
15
+ useCreateDocument,
16
+ } from "@/hooks/use-documents";
14
17
  import { useLocalStorage } from "@/hooks/use-local-storage";
15
18
  import { documentQueryFilter } from "@/lib/document-query";
16
19
  import { markDocumentCreationPending } from "@/lib/optimistic-document";
@@ -34,6 +37,7 @@ export function useCreatePage(opts?: {
34
37
  awaitPersist?: boolean;
35
38
  }) {
36
39
  const navigate = useNavigate();
40
+ const location = useLocation();
37
41
  const queryClient = useQueryClient();
38
42
  const createDocument = useCreateDocument();
39
43
  const contentSpacesQuery = useContentSpaces();
@@ -78,11 +82,19 @@ export function useCreatePage(opts?: {
78
82
  createdAt: now,
79
83
  updatedAt: now,
80
84
  });
85
+ const previousDocuments = queryClient.getQueryData(
86
+ LIST_DOCUMENTS_QUERY_KEY,
87
+ );
88
+ const previousPath = `${location.pathname}${location.search}${location.hash}`;
81
89
 
82
90
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
83
91
  const docs: Document[] =
84
92
  old?.documents ?? (Array.isArray(old) ? old : []);
85
- return { documents: [...docs, tempDoc] };
93
+ if (Array.isArray(old)) return [...docs, tempDoc];
94
+ return {
95
+ ...(old && typeof old === "object" ? old : {}),
96
+ documents: [...docs, tempDoc],
97
+ };
86
98
  });
87
99
  queryClient.setQueryData(["action", "get-document", { id }], tempDoc);
88
100
 
@@ -111,11 +123,18 @@ export function useCreatePage(opts?: {
111
123
  };
112
124
 
113
125
  const onPersistError = (err: unknown) => {
126
+ rollbackOptimisticCreatedDocument(
127
+ queryClient,
128
+ id,
129
+ previousDocuments !== undefined,
130
+ );
114
131
  queryClient.invalidateQueries({
115
132
  queryKey: ["action", "list-documents"],
116
133
  });
117
134
  queryClient.removeQueries(documentQueryFilter(id));
118
- if (shouldNavigate) navigate("/");
135
+ if (shouldNavigate) {
136
+ navigate(previousPath, { replace: true, flushSync: true });
137
+ }
119
138
  toast.error("Failed to create page", {
120
139
  description:
121
140
  err instanceof Error ? err.message : "Something went wrong",
@@ -137,6 +156,9 @@ export function useCreatePage(opts?: {
137
156
  },
138
157
  [
139
158
  createDocument,
159
+ location.hash,
160
+ location.pathname,
161
+ location.search,
140
162
  navigate,
141
163
  onAfterNavigate,
142
164
  queryClient,
@@ -66,6 +66,89 @@ export const LIST_DOCUMENTS_QUERY_KEY = [
66
66
  undefined,
67
67
  ] as const;
68
68
 
69
+ export function restoreListDocumentsSnapshot(
70
+ queryClient: Pick<QueryClient, "removeQueries" | "setQueryData">,
71
+ snapshot: unknown,
72
+ ) {
73
+ if (snapshot === undefined) {
74
+ queryClient.removeQueries({ queryKey: LIST_DOCUMENTS_QUERY_KEY });
75
+ return;
76
+ }
77
+ queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, snapshot);
78
+ }
79
+
80
+ export function rollbackOptimisticCreatedDocument(
81
+ queryClient: Pick<
82
+ QueryClient,
83
+ "getQueryData" | "removeQueries" | "setQueryData"
84
+ >,
85
+ documentId: string,
86
+ hadListSnapshot: boolean,
87
+ ) {
88
+ const current = queryClient.getQueryData(LIST_DOCUMENTS_QUERY_KEY);
89
+ const documents: Document[] = Array.isArray(current)
90
+ ? current
91
+ : ((current as DocumentListResponse | undefined)?.documents ?? []);
92
+ const remaining = documents.filter((document) => document.id !== documentId);
93
+
94
+ if (!hadListSnapshot && remaining.length === 0) {
95
+ queryClient.removeQueries({ queryKey: LIST_DOCUMENTS_QUERY_KEY });
96
+ return;
97
+ }
98
+
99
+ if (Array.isArray(current)) {
100
+ queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, remaining);
101
+ return;
102
+ }
103
+
104
+ queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, {
105
+ ...(current && typeof current === "object" ? current : {}),
106
+ documents: remaining,
107
+ });
108
+ }
109
+
110
+ export function restoreDeletedDocumentSnapshots(
111
+ queryClient: Pick<QueryClient, "getQueryData" | "setQueryData">,
112
+ listSnapshot: unknown,
113
+ documentSnapshots: Array<[readonly unknown[], unknown]>,
114
+ deletedDocumentIds: Iterable<string>,
115
+ ) {
116
+ const deletedIds = new Set(deletedDocumentIds);
117
+ const snapshotDocuments: Document[] = Array.isArray(listSnapshot)
118
+ ? listSnapshot
119
+ : ((listSnapshot as DocumentListResponse | undefined)?.documents ?? []);
120
+ const current = queryClient.getQueryData(LIST_DOCUMENTS_QUERY_KEY);
121
+ const currentDocuments: Document[] = Array.isArray(current)
122
+ ? current
123
+ : ((current as DocumentListResponse | undefined)?.documents ?? []);
124
+ const currentDocumentIds = new Set(
125
+ currentDocuments.map((document) => document.id),
126
+ );
127
+ const restoredDocuments = snapshotDocuments.filter(
128
+ (document) =>
129
+ deletedIds.has(document.id) && !currentDocumentIds.has(document.id),
130
+ );
131
+ const documents = [...currentDocuments, ...restoredDocuments];
132
+
133
+ if (Array.isArray(current)) {
134
+ queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, documents);
135
+ } else {
136
+ queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, {
137
+ ...(current && typeof current === "object"
138
+ ? current
139
+ : listSnapshot && typeof listSnapshot === "object"
140
+ ? listSnapshot
141
+ : {}),
142
+ documents,
143
+ });
144
+ }
145
+ for (const [queryKey, data] of documentSnapshots) {
146
+ if (queryClient.getQueryData(queryKey) === undefined) {
147
+ queryClient.setQueryData(queryKey, data);
148
+ }
149
+ }
150
+ }
151
+
69
152
  const DOCUMENT_LIST_PAGE_SIZE = 200;
70
153
 
71
154
  export async function fetchCompleteDocumentList(
@@ -0,0 +1,482 @@
1
+ # Content navigation coherence: ordered cache-boundary repairs
2
+
3
+ ## Summary
4
+
5
+ Four durable reports describe navigation that temporarily disagrees with
6
+ committed Content state: the tree blanks after page creation, an MCP-created
7
+ file does not appear without reload, a deleted page remains briefly, and a new
8
+ pin takes seconds to appear. They are related as user experience, but current
9
+ repository evidence does **not** support one four-symptom implementation lane.
10
+
11
+ Creation, deletion, and cross-surface MCP creation all converge on the complete
12
+ `list-documents` action query that supplies the document tree. Pinning does not:
13
+ the visible Pinned section is rendered from the Favorites system database's
14
+ `get-content-database` query. The current code already contains partial or
15
+ complete-looking repairs for each path, so historical reports cannot establish
16
+ that any particular failure still reproduces on current head.
17
+
18
+ The smallest compatible first slice is therefore one document-list coherence
19
+ lane, beginning with a current-head successful-user-story reproduction matrix.
20
+ It may repair only the first demonstrated failure at the existing query/cache
21
+ ownership boundary. Pinning stays a separately ordered repair after that lane;
22
+ it must not be folded into a generic navigation store or broad invalidation
23
+ helper.
24
+
25
+ ## Durable task cluster
26
+
27
+ - `ab8d5f7f-2fc7-438a-8c63-74a5e1b42b55` — **Fix Content navigation tree
28
+ flicker after page creation**. Two reporter flows and Clips show the entire
29
+ tree disappearing temporarily after creation.
30
+ - `7438346b-c717-4e2f-b581-a90cb2c91ab8` — **Refresh Content navigation after
31
+ MCP-created files**. A reporter says supported MCP creation remains absent
32
+ until reload.
33
+ - `50c4a2d7-2525-4fa3-8260-6ae3e4beb6f7` — **Remove stale Content navigation
34
+ after page deletion**. A reporter Clip shows confirmed deletion leaving a
35
+ stale entry briefly.
36
+ - `a185e193-28fb-4d0d-a014-e79d4ddcb6c3` — **Make Content pinning update the
37
+ sidebar immediately**. The report says pinning takes about three seconds
38
+ while unpinning is nearly immediate.
39
+
40
+ These are historical reporter observations. This Shape did not replay the
41
+ Clips or mutate a live Content runtime, and it does not claim a fresh
42
+ reproduction.
43
+
44
+ ## Product context
45
+
46
+ - Feature: `content.feature.find-your-place-again` (partially implemented).
47
+ - Capability: `content.navigation.sidebar` (approved shape).
48
+ - User workflow: while Content is open, create, externally create, delete, pin,
49
+ or unpin a Page and continue navigating without the sidebar blanking, lagging,
50
+ or presenting stale success.
51
+ - Product contract preserved: the sidebar is a personal navigation projection,
52
+ not object parentage or a second source of truth. Personal pins must not
53
+ reparent Pages or alter Database membership beyond the Favorites reference.
54
+ - Change classification: contract repair if a current-head reproduction fails;
55
+ verification-only closure for any historical symptom that current evidence
56
+ proves already repaired.
57
+
58
+ ## Current architecture and demonstrated callers
59
+
60
+ ### Document-tree projection
61
+
62
+ `DocumentSidebar` calls `useDocuments()`. That hook owns the stable
63
+ `["action", "list-documents", undefined]` query and refuses to return a clipped
64
+ paginated list as complete. The sidebar derives local-file and database trees
65
+ from that one complete document list.
66
+
67
+ Demonstrated callers entering this projection:
68
+
69
+ 1. **Sidebar page creation.** `handleCreatePage` calls the shared
70
+ `create-document` Action. Ordinary SQL creation and local-file creation
71
+ backed by a Files database insert a temporary document into
72
+ `LIST_DOCUMENTS_QUERY_KEY`; direct local-file creation without that
73
+ database waits for write-back before navigation.
74
+ 2. **Sidebar deletion.** `handleDelete` computes the deleted subtree, removes
75
+ those rows from `LIST_DOCUMENTS_QUERY_KEY`, removes per-document queries,
76
+ and chooses a safe remaining route before calling `delete-document` or
77
+ `delete-content-database`. Success and failure both refetch the list; failure
78
+ does not restore a captured list snapshot directly.
79
+ 3. **MCP/tool creation.** Mutating Action execution emits a durable
80
+ action-change marker. Root `useDbSync()` receives action events and the core
81
+ sync hook invalidates active `["action"]` queries, including
82
+ `list-documents`, while preserving a trailing refresh when an older read is
83
+ already in flight. The framework already intends this to be the cross-tab,
84
+ agent, script, and MCP convergence seam.
85
+
86
+ ### Pinned projection
87
+
88
+ `DocumentSidebar` obtains `favoritesDatabaseId` from `list-content-spaces`,
89
+ reads the Pinned section through `useContentDatabaseById(favoritesDatabaseId)`,
90
+ and renders its `ContentDatabaseItem` rows. Pin/unpin calls `update-document`
91
+ with `isFavorite`.
92
+
93
+ `useUpdateDocument` optimistically patches per-document, `list-documents`, and
94
+ every cached `get-content-database` response and restores captured snapshots on
95
+ error. For a Favorites database, setting `isFavorite: false` removes an
96
+ existing row immediately. Setting it to `true` can only patch an item already
97
+ present; it cannot synthesize the missing Favorites membership row. This
98
+ directly predicts the reported asymmetry—unpin is immediate while pin waits for
99
+ the database query to refetch—but remains an inference until reproduced on
100
+ current head.
101
+
102
+ ### Navigation and application state
103
+
104
+ Route selection (`activeDocumentId`, React Router navigation, selected Content
105
+ space, expanded nodes, and persisted sidebar expansion) controls which existing
106
+ projection is open. It does not own document-list membership or Favorites
107
+ membership. No evidence supports moving these four repairs into application
108
+ state or creating a new navigation store.
109
+
110
+ ## Architecture grounding and fit
111
+
112
+ ### Direct evidence
113
+
114
+ - `templates/content/app/components/sidebar/DocumentSidebar.tsx` renders the
115
+ tree from `useDocuments`, optimistically removes deleted subtrees, and renders
116
+ Pinned from the Favorites `get-content-database` response.
117
+ - `templates/content/app/hooks/use-documents.ts` owns the complete-list query,
118
+ document cache patches, optimistic favorite updates, rollback snapshots, and
119
+ explicit document/list/database invalidation.
120
+ - `templates/content/app/hooks/use-content-database.ts` owns database response
121
+ caches and existing optimistic database-item movement.
122
+ - `templates/content/app/hooks/use-db-sync.ts` installs the Content root's core
123
+ database/action synchronization.
124
+ - `packages/core/src/client/use-db-sync.ts`, action routes, CLI runner, and
125
+ production-agent paths establish action-change events as the intended
126
+ cross-surface invalidation seam.
127
+ - The four Bowerbird notes preserve the reporter observations, distinct outcome
128
+ boundaries, and done conditions.
129
+
130
+ ### Inferences to test
131
+
132
+ - The creation report may arise from a loading-state transition or list-cache
133
+ replacement around ordinary SQL creation, but invalidation alone does not
134
+ prove that cause.
135
+ - The deletion report may reflect an older implementation, a refetch race, a
136
+ content-database deletion path, or rollback behavior; current optimistic
137
+ removal means the historical Clip is not current-head proof.
138
+ - MCP visibility should converge through action-change invalidation; if it does
139
+ not, the first missing boundary may be marker publication, delivery/cursor
140
+ replay, event scoping, or the list refetch—not necessarily React Query.
141
+ - Pin latency is likely the absent optimistic Favorites membership row, not the
142
+ document-list query or navigation state.
143
+
144
+ ### Ownership boundaries
145
+
146
+ - Actions and SQL own committed Page and Favorites membership state.
147
+ - `useDocuments` and `LIST_DOCUMENTS_QUERY_KEY` own the complete document-list
148
+ client projection.
149
+ - `get-content-database` caches own Favorites rows and order.
150
+ - Core `useDbSync` plus action-change markers own cross-surface freshness
151
+ signaling; Content may configure or narrowly consume that seam but must not
152
+ add an MCP-only refresh channel.
153
+ - React Router and Content sidebar state own route, selection, and expansion,
154
+ not object membership.
155
+
156
+ ### Legacy contracts that must remain unchanged
157
+
158
+ - `list-documents` exhaustion remains explicit; absent, unreadable, clipped,
159
+ and successfully empty results stay distinguishable.
160
+ - The dedicated `get-document` response remains authoritative for editable
161
+ bodies; list/database snapshots must not seed a body as fresh.
162
+ - Local-file optimistic creation and write-back behavior remain intact.
163
+ - Deletes navigate safely, remove the full subtree, and roll back visibly on
164
+ failure.
165
+ - Pinning remains personal and access-scoped, with truthful persistence,
166
+ rollback, and no Page reparenting.
167
+ - Action-change sync remains generic across browser, MCP, agent, CLI, and other
168
+ supported Action callers; no provider- or MCP-specific twin path is added.
169
+ - Broad bare-action invalidation is not added to high-volume paths that already
170
+ own narrow invalidation.
171
+
172
+ ### Smallest compatible delta
173
+
174
+ First, characterize all three document-list flows against one mounted sidebar
175
+ and the real supported interfaces. If a document-list symptom fails, fix only
176
+ the earliest demonstrated break in the existing Action -> action-change or
177
+ mutation lifecycle -> `LIST_DOCUMENTS_QUERY_KEY` -> derived tree chain. Preserve
178
+ the previous successful list while a replacement read is pending and use exact
179
+ optimistic patch/rollback only where the failing flow requires it.
180
+
181
+ Do not design a generic `navigation-coherence` cache, an application-state copy,
182
+ or a new subscription. Existing ownership is already specific enough.
183
+
184
+ ### Deferred capabilities
185
+
186
+ - Optimistic construction of a new Favorites `ContentDatabaseItem` and its
187
+ exact rollback/reconciliation strategy.
188
+ - Dynamic Recent/Shared sections, generalized personal References, and the
189
+ complete `content.navigation.sidebar` Capability.
190
+ - Session restoration, view-instance state, local-folder watch behavior, and
191
+ provider-source hydration.
192
+ - Refactoring every mutation to a generalized normalized entity cache.
193
+
194
+ ### Reversibility
195
+
196
+ The first slice is restricted to current query keys, mutation callbacks, sync
197
+ configuration, and focused regression coverage. It adds no schema, route,
198
+ protocol, source-of-truth, or product vocabulary and can be reverted without
199
+ data migration.
200
+
201
+ ### Unresolved owner questions
202
+
203
+ None. Current code and product records settle the public boundaries; the
204
+ remaining uncertainty is empirical current-head behavior for Work to establish.
205
+
206
+ ## Implementation-lane decision
207
+
208
+ Use **two separately ordered repair lanes**, not one four-symptom lane:
209
+
210
+ 1. **Document-list coherence lane (first).** Treat creation, deletion, and MCP
211
+ creation as one investigation/proof lane because they share the same rendered
212
+ query boundary. Within it, repair only symptoms that reproduce, in this
213
+ order: creation flicker, deletion success/rollback, then MCP cross-surface
214
+ convergence. One fix may close siblings only when the complete acceptance
215
+ matrix proves that exact shared delta.
216
+ 2. **Favorites membership lane (second).** Shape/implement immediate pin and
217
+ unpin behavior at the Favorites database cache boundary. It may reuse
218
+ existing database-cache helpers, but it is not coupled to the first lane's
219
+ document-list or sync repair and should not delay it.
220
+
221
+ This ordering puts the highest-confidence shared boundary first while avoiding
222
+ a four-task PR whose mechanism and proof surface would be ambiguous.
223
+
224
+ ## Explicit exclusions
225
+
226
+ - No shipping-code edit, dependency, schema, migration, branch operation,
227
+ commit, push, pull request, deployment, feature-flag change, merge, or
228
+ Bowerbird status mutation is authorized by this Shape.
229
+ - No new navigation store, event bus, subscription, raw REST endpoint, or MCP-
230
+ specific refresh action.
231
+ - No redesign of the sidebar, hierarchy, Content spaces, Home, Recent, Shared,
232
+ session restore, or database Views.
233
+ - No claim that historical reports reproduce on current head.
234
+ - No automatic consolidation or completion of the four Bowerbird tasks; Work
235
+ must reconcile each only from exact current proof.
236
+
237
+ ## Successful-user-story acceptance plan
238
+
239
+ Story `content-navigation-document-list-coherence-v1`:
240
+
241
+ > With an existing populated Content sidebar open, a person can create and
242
+ > delete a Page while a supported Content MCP caller can create another Page;
243
+ > throughout those changes the existing tree never blanks, committed additions
244
+ > appear without reload, confirmed deletions disappear immediately with safe
245
+ > navigation, failed deletion restores the prior truthful state, and incomplete
246
+ > reads never masquerade as an empty workspace.
247
+
248
+ Required assertions:
249
+
250
+ 1. Starting from at least two visible Pages, ordinary UI page creation keeps the
251
+ pre-existing rows visible continuously and adds/navigates to exactly one new
252
+ Page after success.
253
+ 2. Failed UI creation restores the exact prior tree and route with an explicit
254
+ error; it leaves no optimistic row or stale page query.
255
+ 3. Supported MCP creation in the same workspace appears in the already-open
256
+ sidebar without manual reload and without duplicate rows.
257
+ 4. A confirmed UI deletion removes the Page and descendants immediately and
258
+ selects a deterministic safe remaining destination.
259
+ 5. A failed deletion restores the exact prior tree and active route and exposes
260
+ the failure instead of presenting deletion success.
261
+ 6. During slow/in-flight replacement reads, the last complete successful list
262
+ stays visible; unavailable, unauthorized, inconsistent-pagination, and
263
+ successfully empty results remain distinct.
264
+ 7. The same flows do not regress local-file creation, per-document body
265
+ authority, access scoping, or full-list pagination.
266
+
267
+ Acceptance policy:
268
+
269
+ - Modality: `real-interface` joined with focused automated regression coverage.
270
+ - Independence: `preferred`.
271
+ - Custody: `same-context-allowed`.
272
+ - Interface: the real Content UI for UI create/delete plus the supported
273
+ Content MCP Action surface for cross-surface creation, against an isolated
274
+ local or branch-preview workspace with task-owned disposable Pages.
275
+ - Rationale: the defect is interaction timing across two real callers, so unit
276
+ tests alone are insufficient. Alice did not require tester-owned independent
277
+ custody; same-context evidence is proportionate, with technical review and
278
+ automated failure/rollback coverage.
279
+
280
+ Work must declare exact disposable Page IDs and baseline tree state before
281
+ mutation, delete every created Page, and prove independent absence before
282
+ acceptance can be satisfied.
283
+
284
+ The second Favorites lane retains its own acceptance story from Bowerbird:
285
+ pin and unpin update immediately, success reconciles to persisted Favorites
286
+ membership, failure restores the prior state with an explicit error, and
287
+ focused plus real-interface coverage exercises both directions. That story is
288
+ not part of the first Work grant.
289
+
290
+ ## Architecture fingerprint — frozen five
291
+
292
+ ```yaml
293
+ authoritySchemaVersion: 3
294
+ stage: shape
295
+ authority-source: >-
296
+ Delegated request from Codex thread 01a00f83-f02d-7b42-9a31-ff18c5a5eded:
297
+ shape the four named Content navigation tasks only; implementation is not
298
+ authorized.
299
+ authorized-scope:
300
+ repositories:
301
+ - /Users/alicemoore/.codex/worktrees/e19d/agent-native
302
+ product-surfaces:
303
+ - Content document-tree navigation
304
+ outcome: >-
305
+ Prove and repair current-head coherence for UI-created, UI-deleted, and
306
+ MCP-created Pages at the existing complete document-list boundary without
307
+ coupling the separate Favorites membership repair.
308
+ allowed-mutations:
309
+ - artifact-write
310
+ write-targets:
311
+ artifacts:
312
+ - templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md
313
+ governing-artifact:
314
+ path: templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md
315
+ revision: shape-v1-2026-08-18
316
+ architecture-fingerprint:
317
+ outcome: >-
318
+ Existing Content navigation remains truthful and continuously usable while
319
+ UI and MCP Page mutations converge through the document-list projection.
320
+ shipping-surfaces:
321
+ - id: content-document-list-navigation
322
+ repository: builderio/agent-native
323
+ product-surface: templates/content document-tree sidebar and Action sync
324
+ constituency: authorized Content users using UI and supported MCP callers
325
+ durable-destination: agent-native origin/main Content template
326
+ integration-action: merge
327
+ governing-architecture: >-
328
+ Actions and SQL remain authoritative; UI-local mutation lifecycles and the
329
+ existing core action-change sync seam converge the complete
330
+ LIST_DOCUMENTS_QUERY_KEY projection, while route/application state owns
331
+ selection only and Favorites remains a separate database projection.
332
+ acceptance-story:
333
+ id: content-navigation-document-list-coherence-v1
334
+ summary: >-
335
+ A populated open sidebar stays visible and converges without reload across
336
+ UI create, UI delete success and rollback, and supported MCP creation.
337
+ required-assertions:
338
+ - UI creation preserves existing rows and adds exactly one committed Page.
339
+ - Creation failure restores the exact prior tree and route.
340
+ - Supported MCP creation appears without reload or duplication.
341
+ - Deletion removes the subtree and selects a safe destination immediately.
342
+ - Deletion failure restores the exact prior tree and active route.
343
+ - Slow, failed, unreadable, clipped, and empty reads remain distinguishable.
344
+ - Local-file behavior, body authority, access scope, and pagination remain intact.
345
+ acceptance-policy:
346
+ modality: real-interface
347
+ independence: preferred
348
+ custody: same-context-allowed
349
+ interface: >-
350
+ Real Content UI plus supported Content MCP Actions in an isolated local
351
+ or branch-preview workspace with declared disposable Pages.
352
+ rationale: >-
353
+ Timing and cross-caller convergence require real interfaces; Alice did
354
+ not make independent tester custody part of the story.
355
+ risk-strategy:
356
+ kind: system-ready
357
+ production-validation-after-merge: false
358
+ architecture-grounding:
359
+ applicability: required
360
+ reason: >-
361
+ The lane crosses Content mutation hooks and the shared framework
362
+ action-change synchronization seam.
363
+ status: grounded
364
+ demonstrated-callers:
365
+ - DocumentSidebar UI create through create-document.
366
+ - DocumentSidebar UI delete through delete-document or delete-content-database.
367
+ - Supported Content MCP/tool create through the same mutating Action surface.
368
+ existing-primitives:
369
+ - useDocuments and LIST_DOCUMENTS_QUERY_KEY complete-list cache.
370
+ - DocumentSidebar optimistic local-file create and subtree delete patches.
371
+ - useUpdateDocument cache snapshots and rollback.
372
+ - core useDbSync action-change invalidation with trailing refresh.
373
+ ownership-boundaries:
374
+ - SQL and Actions own committed objects and memberships.
375
+ - list-documents owns the complete tree projection.
376
+ - get-content-database owns Favorites membership rows.
377
+ - action-change sync owns cross-surface freshness delivery.
378
+ - Router and sidebar state own selection and expansion only.
379
+ legacy-contracts:
380
+ - Complete-list pagination fails loudly rather than returning a clipped success.
381
+ - get-document remains authoritative for editable bodies.
382
+ - Local-file optimistic creation and write-back remain intact.
383
+ - Delete success, rollback, and safe navigation remain truthful.
384
+ - Action-change stays caller-generic and access-scoped.
385
+ shared-vocabulary:
386
+ - document-list projection
387
+ - Favorites membership projection
388
+ - action-change sync seam
389
+ smallest-compatible-delta: >-
390
+ Reproduce the three document-list flows on current head, then repair only
391
+ the earliest demonstrated break inside the existing mutation/action-change
392
+ to LIST_DOCUMENTS_QUERY_KEY chain, preserving the last complete list and
393
+ adding exact rollback where the failed story requires it.
394
+ deferred-capabilities:
395
+ - Immediate optimistic Favorites membership creation and rollback.
396
+ - Full personal References and dynamic sidebar sections.
397
+ - Session restoration, Home, Recent, Shared, and provider/local-source sync.
398
+ reversibility: >-
399
+ No schema or public contract changes; the slice stays within existing query
400
+ keys, mutation callbacks, sync configuration, and regression coverage.
401
+ direct-evidence:
402
+ - templates/content/app/components/sidebar/DocumentSidebar.tsx
403
+ - templates/content/app/hooks/use-documents.ts
404
+ - templates/content/app/hooks/use-content-database.ts
405
+ - templates/content/app/hooks/use-db-sync.ts
406
+ - packages/core/src/client/use-db-sync.ts
407
+ - templates/content/docs/product/capabilities/content.navigation.sidebar.md
408
+ - the four named Bowerbird task notes
409
+ inferences:
410
+ - Pin asymmetry likely comes from inability to add an absent optimistic Favorites row.
411
+ - Historical creation, deletion, and MCP failures may have changed on current head.
412
+ unresolved-owner-questions: []
413
+ delegation-ceiling: []
414
+ product-boundary-gates:
415
+ agent-native-public-constituency: >-
416
+ Content is a public template surface for authorized end users and supported
417
+ MCP callers; the repository evidence names both callers and one shared Action contract.
418
+ bowerbird-product-boundary: >-
419
+ Four existing tasks remain separate durable outcomes; this artifact orders
420
+ their repair and does not mutate or collapse their status.
421
+ acceptance-state:
422
+ status: satisfied
423
+ summary: >-
424
+ Work preserves the complete list-cache shape during optimistic creation,
425
+ removes only the failed creation's temporary Page, and restores exact list
426
+ and document snapshots on failed deletion. Sixty-two focused tests,
427
+ typecheck, all 55 guards, a
428
+ production build, real UI creation/deletion, and open-UI Action refresh are
429
+ green. Every declared disposable Page is independently absent after cleanup.
430
+ blockers: []
431
+ last-land-packet: null
432
+ ledger-revision: content-navigation-coherence-work-v1
433
+ status: review-ready
434
+ ```
435
+
436
+ ## Work evidence — 2026-08-18
437
+
438
+ - Exact branch: `codex/content-navigation-coherence` from
439
+ `origin/main@39383b558b881269a1387d7cfcc94eba8826250b`.
440
+ - Smallest delta: preserve the existing list-cache envelope during optimistic
441
+ creation; remove only the failed creation's temporary Page so concurrent
442
+ creates survive; restore deletion list/per-document snapshots and the exact
443
+ prior URL synchronously on mutation failure; remove the optimistic list query
444
+ when no prior snapshot existed.
445
+ - Cross-surface result: two Action-created Pages appeared in the already-open
446
+ real sidebar without reload or duplication, so no new MCP channel or core
447
+ sync change was necessary.
448
+ - Real UI result: with `nav-baseline-a` and `nav-baseline-b` visible, creating
449
+ `nav-ui-create` kept both baseline rows visible and navigated to exactly one
450
+ new Page. Confirmed deletion removed it immediately and navigated to the
451
+ surviving `Dev's workspace` surface while both baseline rows remained.
452
+ - Automated result: 62 focused Content tests pass, including concurrent and
453
+ absent-list creation rollback, exact prior-URL restoration, complete-list
454
+ pagination, and deleted list/per-document snapshot restoration. The 44
455
+ focused core sync tests also pass.
456
+ - Repository result: Content typecheck, `git diff --check`, all 55 guards, and
457
+ the Content production build pass. The build reports pre-existing doctor and
458
+ local-production-configuration warnings but completes successfully.
459
+ - Review result: an independent read-only review found two rollback holes
460
+ (prior-route restoration and absent-snapshot semantics) plus weak deletion
461
+ coverage; all three were corrected and covered before acceptance.
462
+ - Cleanup receipt: `nav-baseline-a`, `nav-baseline-b`, and
463
+ `thmKsbqMzOAJ` (`nav-ui-create`) were permanently deleted. The open sidebar
464
+ reported zero matching rows, SQLite reported zero matching records, port
465
+ `8086` had no listener after shutdown, and
466
+ `/tmp/content-nav-coherence.CFh4Jg` was moved to Trash.
467
+ - Compute receipt: framework remote preflight could not run because Tailscale
468
+ was stopped, so the supported Mac fallback was used; no remote workload or
469
+ manifest was created.
470
+
471
+ ## Precise Work handoff
472
+
473
+ Invoke:
474
+
475
+ `/work templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md`
476
+
477
+ Work is authorized only after that explicit invocation. It should begin with
478
+ the document-list acceptance matrix, preserve the exact frozen fingerprint,
479
+ and stop rather than absorbing the deferred Favorites lane. When the first lane
480
+ is proven and review-ready, reconcile each of the three document-list Bowerbird
481
+ tasks independently from its evidence; leave the pinning task open for its
482
+ separate ordered repair.
@@ -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
  }>>;
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
+ error?: undefined;
14
15
  count: number;
15
16
  updated?: undefined;
16
- error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
+ error?: undefined;
19
20
  count?: undefined;
20
21
  updated: number;
21
- error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  count?: undefined;
30
31
  updated?: undefined;
31
- error?: undefined;
32
32
  ok: boolean;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ ok?: undefined;
45
+ error?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
47
- error?: undefined;
48
- ok?: undefined;
49
49
  } | {
50
+ ok?: undefined;
51
+ error?: undefined;
50
52
  summary?: undefined;
51
53
  spans?: undefined;
52
54
  id: string;
53
- error?: undefined;
54
- ok?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  id?: undefined;
59
60
  error: any;
60
- ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -1242,7 +1242,7 @@ export function prepareDesktopOAuthBrowserBinding(event) {
1242
1242
  if (!binding || !/^[A-Za-z0-9_-]{43}$/.test(binding)) {
1243
1243
  binding = crypto.randomBytes(32).toString("base64url");
1244
1244
  setCookie(event, DESKTOP_OAUTH_BROWSER_BINDING_COOKIE, binding, {
1245
- ...crossSiteCookieAttrs(event),
1245
+ ...desktopOAuthBrowserBindingCookieAttrs(event),
1246
1246
  httpOnly: true,
1247
1247
  path: "/",
1248
1248
  maxAge: Math.floor(DESKTOP_EXCHANGE_TTL_MS / 1_000),
@@ -2810,6 +2810,18 @@ function crossSiteCookieAttrs(event) {
2810
2810
  ? { sameSite: "none", secure: true, partitioned: true }
2811
2811
  : { sameSite: "lax", secure: false };
2812
2812
  }
2813
+ /**
2814
+ * The binding cookie is set before navigating to Google and read after the
2815
+ * provider redirects back. A partitioned cookie uses the top-level site from
2816
+ * the bootstrap request, so it is unavailable when the callback starts from
2817
+ * Google's top-level site. Keep this host-scoped cookie unpartitioned while
2818
+ * retaining the cross-site and transport protections required by the flow.
2819
+ */
2820
+ function desktopOAuthBrowserBindingCookieAttrs(event) {
2821
+ return isHttpsRequest(event)
2822
+ ? { sameSite: "none", secure: true }
2823
+ : { sameSite: "lax", secure: false };
2824
+ }
2813
2825
  function setFirstRunOnboardingCookie(event) {
2814
2826
  setCookie(event, FIRST_RUN_ONBOARDING_COOKIE, "1", {
2815
2827
  ...crossSiteCookieAttrs(event),
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.0.0-beta-20260819204836",
3
+ "version": "0.0.0-beta-20260819215811",
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": {