@agent-native/core 0.154.0 → 0.154.2

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 (54) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-12-feature-flags-can-be-managed-across-apps-whose-workspace-rec.md +6 -0
  3. package/corpus/templates/analytics/changelog/2026-08-12-kept-feature-flag-details-readable-by-stacking-rollout-contr.md +6 -0
  4. package/corpus/templates/analytics/scripts/preview-dashboard-report.ts +16 -5
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +98 -31
  6. package/corpus/templates/content/actions/resolve-content-landing.ts +162 -32
  7. package/corpus/templates/content/shared/content-landing.ts +5 -0
  8. package/corpus/templates/dispatch/changelog/2026-08-12-dispatch-now-appears-in-authorized-workspace-app-directories.md +6 -0
  9. package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +3 -4
  10. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +15 -3
  11. package/corpus/templates/slides/actions/patch-deck.ts +12 -3
  12. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +73 -1
  13. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -1
  14. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -0
  15. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +15 -0
  16. package/corpus/templates/slides/app/context/DeckContext.tsx +19 -31
  17. package/corpus/templates/slides/app/hooks/use-navigation-state.ts +15 -16
  18. package/corpus/templates/slides/app/pages/DeckEditor.tsx +9 -0
  19. package/corpus/templates/slides/changelog/2026-08-13-clicking-a-slide-thumbnail-now-focuses-it-so-the-slide-shortc.md +6 -0
  20. package/corpus/templates/slides/changelog/2026-08-13-duplicating-or-restoring-a-slide-now-keeps-its-transition-ani.md +6 -0
  21. package/corpus/templates/slides/changelog/2026-08-13-opening-presentation-mode-from-the-agent-now-starts-on-the-re.md +6 -0
  22. package/corpus/templates/slides/changelog/2026-08-13-slide-transitions-can-be-set-from-a-visible-control-in-the-sl.md +6 -0
  23. package/dist/a2a/audience.d.ts +2 -0
  24. package/dist/a2a/audience.js +23 -0
  25. package/dist/a2a/client.d.ts +4 -0
  26. package/dist/a2a/client.js +32 -14
  27. package/dist/a2a/server.d.ts +9 -4
  28. package/dist/a2a/server.js +52 -25
  29. package/dist/a2a/types.d.ts +4 -4
  30. package/dist/a2a-claims.d.ts +1 -0
  31. package/dist/a2a-claims.js +3 -1
  32. package/dist/client/feature-flags/FeatureFlagsPanel.js +2 -2
  33. package/dist/collab/awareness.d.ts +2 -2
  34. package/dist/collab/routes.d.ts +1 -1
  35. package/dist/feature-flags/a2a-action-route.js +10 -1
  36. package/dist/feature-flags/actions/set-feature-flag.d.ts +2 -1
  37. package/dist/feature-flags/actions/set-feature-flag.js +9 -2
  38. package/dist/mcp/org-directory.d.ts +3 -0
  39. package/dist/mcp/org-directory.js +15 -5
  40. package/dist/notifications/routes.d.ts +1 -1
  41. package/dist/observability/routes.d.ts +6 -6
  42. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  43. package/dist/resources/handlers.d.ts +1 -1
  44. package/dist/secrets/routes.d.ts +3 -3
  45. package/dist/server/action-routes.js +10 -2
  46. package/dist/server/agent-capabilities.js +0 -0
  47. package/dist/server/agent-chat/action-filters-a2a.d.ts +14 -13
  48. package/dist/server/agent-chat/action-filters-a2a.js +59 -23
  49. package/dist/server/agent-discovery.d.ts +6 -2
  50. package/dist/server/agent-discovery.js +16 -9
  51. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  52. package/dist/server/identity-sso-store.d.ts +5 -4
  53. package/dist/server/identity-sso-store.js +5 -4
  54. package/package.json +1 -1
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: 8201
34
+ - template files: 8208
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Feature flags can be managed across apps whose workspace records use different local IDs
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Kept feature flag details readable by stacking rollout controls at narrow widths.
@@ -19,7 +19,8 @@
19
19
  */
20
20
 
21
21
  import { mkdir, writeFile } from "node:fs/promises";
22
- import { join } from "node:path";
22
+ import path, { join } from "node:path";
23
+ import { pathToFileURL } from "node:url";
23
24
 
24
25
  import { runWithRequestContext } from "@agent-native/core/server/request-context";
25
26
  import { eq } from "drizzle-orm";
@@ -215,7 +216,17 @@ async function renderPreview(
215
216
  if (rendered.degradedPanelIds.length) process.exitCode = 1;
216
217
  }
217
218
 
218
- main().catch((err) => {
219
- console.error("[preview] failed:", err);
220
- process.exit(1);
221
- });
219
+ function isDirectRun(): boolean {
220
+ const entrypoint = process.argv[1];
221
+ return Boolean(
222
+ entrypoint &&
223
+ import.meta.url === pathToFileURL(path.resolve(entrypoint)).href,
224
+ );
225
+ }
226
+
227
+ if (isDirectRun()) {
228
+ main().catch((err) => {
229
+ console.error("[preview] failed:", err);
230
+ process.exit(1);
231
+ });
232
+ }
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
 
3
3
  import { signA2AToken } from "@agent-native/core/a2a";
4
4
  import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
5
+ import { getOrgDomain } from "@agent-native/core/org";
5
6
 
6
7
  import type { AnalyticsAdminContext } from "./db-admin-connections.js";
7
8
 
@@ -31,11 +32,11 @@ export interface WorkspaceFeatureFlagsResult {
31
32
  }
32
33
 
33
34
  export interface WorkspaceFeatureFlagMutationResult {
34
- contractVersion: 1;
35
+ contractVersion: 2;
35
36
  status: "ready";
36
37
  key: string;
37
38
  rules: Record<string, unknown>;
38
- scope: { orgId: string | null };
39
+ scope: { orgId: string | null; orgDomain: string | null };
39
40
  }
40
41
 
41
42
  export interface WorkspaceFeatureFlagMutationInput {
@@ -66,14 +67,14 @@ export function validateWorkspaceFeatureFlagMutation(
66
67
  body: unknown,
67
68
  expected: {
68
69
  key: string;
69
- orgId: string;
70
+ orgDomain: string;
70
71
  rules?: Record<string, unknown>;
71
72
  enabledForEmail?: string;
72
73
  },
73
74
  ): WorkspaceFeatureFlagMutationResult {
74
75
  const payload = body as Partial<WorkspaceFeatureFlagMutationResult> | null;
75
76
  const valid =
76
- payload?.contractVersion === 1 &&
77
+ payload?.contractVersion === 2 &&
77
78
  payload.status === "ready" &&
78
79
  payload.key === expected.key &&
79
80
  !!payload.rules &&
@@ -81,7 +82,7 @@ export function validateWorkspaceFeatureFlagMutation(
81
82
  !Array.isArray(payload.rules) &&
82
83
  !!payload.scope &&
83
84
  typeof payload.scope === "object" &&
84
- payload.scope.orgId === expected.orgId;
85
+ payload.scope.orgDomain === expected.orgDomain;
85
86
  if (!valid)
86
87
  throw new Error(
87
88
  "The target app returned an unsupported or unverified feature flag mutation response.",
@@ -152,13 +153,43 @@ async function delegatedToken(
152
153
  admin: AnalyticsAdminContext,
153
154
  origin: string,
154
155
  scope: "flags:read" | "flags:write",
156
+ resolvedOrgDomain?: string,
155
157
  ): Promise<string> {
156
- return signA2AToken(admin.userEmail, undefined, undefined, {
157
- expiresIn: "120s",
158
- preferGlobalSecret: true,
159
- audience: origin,
160
- extraClaims: { org_id: admin.orgId, scope, jti: randomUUID() },
161
- });
158
+ try {
159
+ const orgDomain =
160
+ resolvedOrgDomain ??
161
+ (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
162
+ if (!orgDomain) throw new TargetCallFailure("token-generation");
163
+ return await signA2AToken(admin.userEmail, orgDomain, undefined, {
164
+ expiresIn: "120s",
165
+ preferGlobalSecret: true,
166
+ audience: origin,
167
+ extraClaims: { org_id: admin.orgId, scope, jti: randomUUID() },
168
+ });
169
+ } catch {
170
+ throw new TargetCallFailure("token-generation");
171
+ }
172
+ }
173
+
174
+ type TargetFailureReason = "token-generation" | "timeout" | "network";
175
+
176
+ class TargetCallFailure extends Error {
177
+ constructor(readonly reason: TargetFailureReason) {
178
+ super(`Feature flag target call failed: ${reason}`);
179
+ this.name = "TargetCallFailure";
180
+ }
181
+ }
182
+
183
+ export function classifyWorkspaceFeatureFlagTargetFailure(
184
+ error: unknown,
185
+ ): TargetFailureReason {
186
+ if (error instanceof TargetCallFailure) return error.reason;
187
+ if (
188
+ error instanceof Error &&
189
+ (error.name === "TimeoutError" || error.name === "AbortError")
190
+ )
191
+ return "timeout";
192
+ return "network";
162
193
  }
163
194
 
164
195
  async function callTarget(
@@ -166,25 +197,34 @@ async function callTarget(
166
197
  admin: AnalyticsAdminContext,
167
198
  action: "list-feature-flags" | "set-feature-flag",
168
199
  body: Record<string, unknown>,
200
+ orgDomain?: string,
169
201
  ): Promise<{ status: number; body: unknown }> {
170
202
  const origin = targetOrigin(app);
171
203
  const token = await delegatedToken(
172
204
  admin,
173
205
  origin,
174
206
  action === "list-feature-flags" ? "flags:read" : "flags:write",
207
+ orgDomain,
175
208
  );
176
- const response = await fetch(`${origin}/_agent-native/actions/${action}`, {
177
- method: action === "list-feature-flags" ? "GET" : "POST",
178
- headers: {
179
- Authorization: `Bearer ${token}`,
180
- Accept: "application/json",
181
- ...(action === "set-feature-flag"
182
- ? { "Content-Type": "application/json" }
183
- : {}),
184
- },
185
- ...(action === "set-feature-flag" ? { body: JSON.stringify(body) } : {}),
186
- signal: AbortSignal.timeout(TARGET_TIMEOUT_MS),
187
- });
209
+ let response: Response;
210
+ try {
211
+ response = await fetch(`${origin}/_agent-native/actions/${action}`, {
212
+ method: action === "list-feature-flags" ? "GET" : "POST",
213
+ headers: {
214
+ Authorization: `Bearer ${token}`,
215
+ Accept: "application/json",
216
+ ...(action === "set-feature-flag"
217
+ ? { "Content-Type": "application/json" }
218
+ : {}),
219
+ },
220
+ ...(action === "set-feature-flag" ? { body: JSON.stringify(body) } : {}),
221
+ signal: AbortSignal.timeout(TARGET_TIMEOUT_MS),
222
+ });
223
+ } catch (error) {
224
+ throw new TargetCallFailure(
225
+ classifyWorkspaceFeatureFlagTargetFailure(error),
226
+ );
227
+ }
188
228
  let parsed: unknown = null;
189
229
  try {
190
230
  parsed = await response.json();
@@ -209,7 +249,7 @@ export function classifyWorkspaceFeatureFlagList(
209
249
  if (result.status === 404 || result.status === 405)
210
250
  return { ...base, state: "unsupported" };
211
251
  if (result.status < 200 || result.status >= 300)
212
- return { ...base, state: "unknown-legacy" };
252
+ return { ...base, state: "unknown-legacy", reason: "target-execution" };
213
253
  const payload = result.body as {
214
254
  flags?: unknown;
215
255
  canManage?: unknown;
@@ -254,7 +294,11 @@ async function mapBounded<T, R>(
254
294
  export async function listWorkspaceFeatureFlags(
255
295
  admin: AnalyticsAdminContext,
256
296
  ): Promise<WorkspaceFeatureFlagsResult> {
257
- const apps = await fetchOrgApps({ serviceOrgId: admin.orgId });
297
+ const apps = await fetchOrgApps({
298
+ selfId: "analytics",
299
+ includeDirectoryApp: true,
300
+ serviceOrgId: admin.orgId,
301
+ });
258
302
  if (apps.length === 0) return { directoryStatus: "unavailable", apps: [] };
259
303
  const entries = await mapBounded(apps, async (app) => {
260
304
  try {
@@ -262,13 +306,14 @@ export async function listWorkspaceFeatureFlags(
262
306
  app,
263
307
  await callTarget(app, admin, "list-feature-flags", {}),
264
308
  );
265
- } catch {
309
+ } catch (error) {
266
310
  return {
267
311
  appId: app.id,
268
312
  appName: app.name,
269
313
  appOrigin: targetOrigin(app),
270
314
  state: "unreachable" as const,
271
315
  flags: [],
316
+ reason: classifyWorkspaceFeatureFlagTargetFailure(error),
272
317
  };
273
318
  }
274
319
  });
@@ -279,14 +324,31 @@ export async function setWorkspaceFeatureFlag(
279
324
  admin: AnalyticsAdminContext,
280
325
  input: WorkspaceFeatureFlagMutationInput,
281
326
  ): Promise<WorkspaceFeatureFlagMutationResult> {
282
- const apps = await fetchOrgApps({ serviceOrgId: admin.orgId });
327
+ const apps = await fetchOrgApps({
328
+ selfId: "analytics",
329
+ includeDirectoryApp: true,
330
+ serviceOrgId: admin.orgId,
331
+ });
283
332
  const app = apps.find((candidate) => candidate.id === input.appId);
284
333
  if (!app)
285
334
  throw new Error(
286
335
  "The requested app is not available in this organization directory.",
287
336
  );
288
337
  const targetInput = workspaceFeatureFlagTargetInput(input);
289
- const result = await callTarget(app, admin, "set-feature-flag", targetInput);
338
+ let orgDomain: string | undefined;
339
+ try {
340
+ orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
341
+ } catch {
342
+ throw new TargetCallFailure("token-generation");
343
+ }
344
+ if (!orgDomain) throw new TargetCallFailure("token-generation");
345
+ const result = await callTarget(
346
+ app,
347
+ admin,
348
+ "set-feature-flag",
349
+ targetInput,
350
+ orgDomain,
351
+ );
290
352
  if (result.status === 401 || result.status === 403)
291
353
  throw new Error("The target app denied this delegated flag operation.");
292
354
  if (result.status === 404 || result.status === 405)
@@ -297,7 +359,7 @@ export async function setWorkspaceFeatureFlag(
297
359
  );
298
360
  return validateWorkspaceFeatureFlagMutation(result.body, {
299
361
  key: input.key,
300
- orgId: admin.orgId,
362
+ orgDomain,
301
363
  ...(input.operation === "replace-rules" && input.rules
302
364
  ? {
303
365
  rules: {
@@ -324,7 +386,11 @@ export async function getWorkspaceFlagTarget(
324
386
  admin: AnalyticsAdminContext,
325
387
  appId: string,
326
388
  ): Promise<FleetFlagApp> {
327
- const apps = await fetchOrgApps({ serviceOrgId: admin.orgId });
389
+ const apps = await fetchOrgApps({
390
+ selfId: "analytics",
391
+ includeDirectoryApp: true,
392
+ serviceOrgId: admin.orgId,
393
+ });
328
394
  const app = apps.find((candidate) => candidate.id === appId);
329
395
  if (!app)
330
396
  throw new Error(
@@ -335,13 +401,14 @@ export async function getWorkspaceFlagTarget(
335
401
  app,
336
402
  await callTarget(app, admin, "list-feature-flags", {}),
337
403
  );
338
- } catch {
404
+ } catch (error) {
339
405
  return {
340
406
  appId: app.id,
341
407
  appName: app.name,
342
408
  appOrigin: targetOrigin(app),
343
409
  state: "unreachable",
344
410
  flags: [],
411
+ reason: classifyWorkspaceFeatureFlagTargetFailure(error),
345
412
  };
346
413
  }
347
414
  }
@@ -1,15 +1,21 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
 
3
3
  import { defineAction } from "@agent-native/core";
4
- import { readAppState } from "@agent-native/core/application-state";
4
+ import {
5
+ compareAndSetAppState,
6
+ listAppState,
7
+ readAppState,
8
+ } from "@agent-native/core/application-state";
5
9
  import { runWithRequestContext } from "@agent-native/core/server";
6
10
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
7
11
  import { z } from "zod";
8
12
 
9
13
  import {
10
14
  CONTENT_LAST_LOCATION_STATE_KEY,
15
+ CONTENT_WELCOME_PAGE_STATE_KEY,
11
16
  type ContentLandingResolution,
12
17
  type ContentLastLocationState,
18
+ type ContentWelcomePageState,
13
19
  } from "../shared/content-landing.js";
14
20
  import { resolveContentDocumentAccess } from "./_content-document-access.js";
15
21
  import { normalizeContentSpaceEmail } from "./_content-space-access.js";
@@ -24,12 +30,85 @@ const WELCOME_CONTENT = `Start here with a page that is wholly yours.
24
30
  - Use the sidebar to find and organize your work.
25
31
  - Ask the agent when you want a hand.`;
26
32
 
27
- function welcomeDocumentId(userEmail: string) {
33
+ function legacyWelcomeDocumentId(userEmail: string, generation: number) {
28
34
  const identity = normalizeContentSpaceEmail(userEmail);
29
- const digest = createHash("sha256").update(identity).digest("hex");
35
+ const input = generation === 0 ? identity : `${identity}:${generation}`;
36
+ const digest = createHash("sha256").update(input).digest("hex");
30
37
  return `content_welcome_${digest.slice(0, 32)}`;
31
38
  }
32
39
 
40
+ function randomWelcomeDocumentId() {
41
+ return `content_welcome_${randomUUID().replace(/-/g, "")}`;
42
+ }
43
+
44
+ function welcomeGeneration(state: Record<string, unknown>): number {
45
+ const candidate = state as ContentWelcomePageState;
46
+ if (
47
+ typeof candidate.generation !== "number" ||
48
+ !Number.isSafeInteger(candidate.generation) ||
49
+ candidate.generation < 0
50
+ ) {
51
+ throw new Error("Content welcome page state has an invalid generation");
52
+ }
53
+ return candidate.generation;
54
+ }
55
+
56
+ function isUniqueConstraintError(error: unknown): boolean {
57
+ let current: unknown = error;
58
+ const seen = new Set<unknown>();
59
+ while (current && typeof current === "object" && !seen.has(current)) {
60
+ seen.add(current);
61
+ const candidate = current as {
62
+ code?: unknown;
63
+ message?: unknown;
64
+ cause?: unknown;
65
+ };
66
+ const code = String(candidate.code ?? "");
67
+ const message = String(candidate.message ?? "");
68
+ if (
69
+ code === "23505" ||
70
+ code.includes("SQLITE_CONSTRAINT") ||
71
+ /unique constraint|unique violation|duplicate key/i.test(message)
72
+ ) {
73
+ return true;
74
+ }
75
+ current = candidate.cause;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ async function resolveWelcomeState(): Promise<{
81
+ generation: number;
82
+ documentId: string | null;
83
+ expectedValue: Record<string, unknown>;
84
+ }> {
85
+ while (true) {
86
+ const entries = await listAppState(CONTENT_WELCOME_PAGE_STATE_KEY);
87
+ const entry = entries.find(
88
+ ({ key }) => key === CONTENT_WELCOME_PAGE_STATE_KEY,
89
+ );
90
+ if (entry) {
91
+ if (!entry.value || typeof entry.value !== "object") {
92
+ throw new Error("Content welcome page state must be an object");
93
+ }
94
+ return {
95
+ generation: welcomeGeneration(entry.value),
96
+ documentId:
97
+ typeof entry.value.documentId === "string" && entry.value.documentId
98
+ ? entry.value.documentId
99
+ : null,
100
+ expectedValue: entry.value,
101
+ };
102
+ }
103
+
104
+ const initial = { generation: 0 };
105
+ if (
106
+ await compareAndSetAppState(CONTENT_WELCOME_PAGE_STATE_KEY, null, initial)
107
+ ) {
108
+ return { generation: 0, documentId: null, expectedValue: initial };
109
+ }
110
+ }
111
+ }
33
112
  function savedDocumentId(state: Record<string, unknown> | null): string | null {
34
113
  const candidate = state as ContentLastLocationState | null;
35
114
  return typeof candidate?.documentId === "string" && candidate.documentId
@@ -44,22 +123,28 @@ async function resolveUsableDocument(documentId: string) {
44
123
  return access.resource;
45
124
  }
46
125
 
47
- async function resolveWelcomeDocument(userEmail: string) {
48
- const documentId = welcomeDocumentId(userEmail);
49
- const document = await resolveUsableDocument(documentId);
50
- if (!document) return null;
126
+ async function resolveWelcomeDocument(userEmail: string, documentId: string) {
127
+ const access = await resolveContentDocumentAccess(documentId);
128
+ const document = access?.resource;
129
+ if (!document) {
130
+ return (await isSoftDeletedDatabaseDocument(documentId))
131
+ ? { status: "unavailable" as const }
132
+ : { status: "missing" as const };
133
+ }
134
+ if (document.trashedAt || (await isSoftDeletedDatabaseDocument(documentId))) {
135
+ return { status: "unavailable" as const };
136
+ }
51
137
 
52
138
  const normalizedEmail = normalizeContentSpaceEmail(userEmail);
53
139
  if (
54
140
  normalizeContentSpaceEmail(document.ownerEmail) !== normalizedEmail ||
55
141
  document.spaceId !== personalContentSpaceId(normalizedEmail) ||
56
142
  document.parentId !== null ||
57
- document.visibility !== "private" ||
58
- document.title !== WELCOME_TITLE
143
+ document.visibility !== "private"
59
144
  ) {
60
- return null;
145
+ return { status: "unavailable" as const };
61
146
  }
62
- return documentId;
147
+ return { status: "usable" as const, documentId };
63
148
  }
64
149
 
65
150
  async function resolveWelcome(userEmail: string): Promise<{
@@ -69,27 +154,72 @@ async function resolveWelcome(userEmail: string): Promise<{
69
154
  "welcome-created" | "welcome-reused"
70
155
  >;
71
156
  }> {
72
- const existing = await resolveWelcomeDocument(userEmail);
73
- if (existing) return { documentId: existing, resolution: "welcome-reused" };
74
-
75
- const documentId = welcomeDocumentId(userEmail);
76
157
  const normalizedEmail = normalizeContentSpaceEmail(userEmail);
77
- try {
78
- await runWithRequestContext({ userEmail: normalizedEmail }, () =>
79
- createDocumentAction.run({
80
- id: documentId,
81
- title: WELCOME_TITLE,
82
- content: WELCOME_CONTENT,
83
- }),
84
- );
85
- return { documentId, resolution: "welcome-created" };
86
- } catch (error) {
87
- // The stable per-user document ID is the cross-request convergence point.
88
- // A competing request can own the insert; only reuse it after the complete
89
- // personal-page invariant has been rechecked.
90
- const raced = await resolveWelcomeDocument(userEmail);
91
- if (raced) return { documentId: raced, resolution: "welcome-reused" };
92
- throw error;
158
+ while (true) {
159
+ const state = await resolveWelcomeState();
160
+ const documentId =
161
+ state.documentId ??
162
+ legacyWelcomeDocumentId(normalizedEmail, state.generation);
163
+ const existing = await resolveWelcomeDocument(normalizedEmail, documentId);
164
+ if (existing.status === "usable") {
165
+ if (!state.documentId) {
166
+ await compareAndSetAppState(
167
+ CONTENT_WELCOME_PAGE_STATE_KEY,
168
+ state.expectedValue,
169
+ { ...state.expectedValue, documentId },
170
+ );
171
+ }
172
+ return {
173
+ documentId: existing.documentId,
174
+ resolution: "welcome-reused",
175
+ };
176
+ }
177
+
178
+ if (!state.documentId || existing.status === "unavailable") {
179
+ await compareAndSetAppState(
180
+ CONTENT_WELCOME_PAGE_STATE_KEY,
181
+ state.expectedValue,
182
+ {
183
+ ...state.expectedValue,
184
+ generation: state.generation + 1,
185
+ documentId: randomWelcomeDocumentId(),
186
+ },
187
+ );
188
+ continue;
189
+ }
190
+
191
+ try {
192
+ await runWithRequestContext({ userEmail: normalizedEmail }, () =>
193
+ createDocumentAction.run({
194
+ id: documentId,
195
+ title: WELCOME_TITLE,
196
+ content: WELCOME_CONTENT,
197
+ }),
198
+ );
199
+ return { documentId, resolution: "welcome-created" };
200
+ } catch (error) {
201
+ const raced = await resolveWelcomeDocument(normalizedEmail, documentId);
202
+ if (raced.status === "usable") {
203
+ return {
204
+ documentId: raced.documentId,
205
+ resolution: "welcome-reused",
206
+ };
207
+ }
208
+ if (raced.status === "unavailable") continue;
209
+ if (isUniqueConstraintError(error)) {
210
+ await compareAndSetAppState(
211
+ CONTENT_WELCOME_PAGE_STATE_KEY,
212
+ state.expectedValue,
213
+ {
214
+ ...state.expectedValue,
215
+ generation: state.generation + 1,
216
+ documentId: randomWelcomeDocumentId(),
217
+ },
218
+ );
219
+ continue;
220
+ }
221
+ throw error;
222
+ }
93
223
  }
94
224
  }
95
225
 
@@ -1,9 +1,14 @@
1
1
  export const CONTENT_LAST_LOCATION_STATE_KEY = "content-last-location-v1";
2
+ export const CONTENT_WELCOME_PAGE_STATE_KEY = "content-welcome-page-v1";
2
3
 
3
4
  export type ContentLastLocationState = {
4
5
  documentId: string;
5
6
  };
6
7
 
8
+ export type ContentWelcomePageState = {
9
+ generation: number;
10
+ documentId?: string;
11
+ };
7
12
  export type ContentLandingResolution =
8
13
  | "restored"
9
14
  | "welcome-created"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-12
4
+ ---
5
+
6
+ Dispatch now appears in authorized workspace app directories for fleet-wide administration
@@ -278,14 +278,13 @@ export interface DiscoveredAppLike {
278
278
 
279
279
  /**
280
280
  * Shape the discovered-agent list into the directory response. The input is
281
- * Dispatch's EXISTING connected-apps registry — `discoverAgents("dispatch")`
281
+ * Dispatch's EXISTING connected-apps registry — `discoverAgents()`
282
282
  * from `@agent-native/core/server/agent-discovery`, the same source
283
283
  * `list-connected-agents` and the `call-agent` delegation path use. It is
284
284
  * already allow-list-respecting (hidden first-party templates are excluded
285
285
  * from `BUILTIN_AGENTS` unless `defaultAgent`), so no second filter is
286
- * needed here; we only drop entries without a usable absolute http(s) URL
287
- * and Dispatch itself (a caller does not need to discover the directory it
288
- * just called).
286
+ * needed here; we only drop entries without a usable absolute http(s) URL.
287
+ * Callers that identify themselves may also request self-filtering.
289
288
  */
290
289
  export function buildOrgAppsResponse(input: {
291
290
  org: string;
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * APP-LIST SOURCE — Dispatch's existing connected-apps registry.
26
26
  * -------------------------------------------------------------
27
- * Dispatch already has a connected-apps concept: `discoverAgents("dispatch")`
27
+ * Dispatch already has a connected-apps concept: `discoverAgents()`
28
28
  * from `@agent-native/core/server/agent-discovery` (the same source
29
29
  * `list-connected-agents` and the `call-agent` delegation path use). It
30
30
  * returns the allow-listed first-party apps with their prod URLs PLUS any
@@ -143,7 +143,16 @@ const orgAppsHandler = defineEventHandler(
143
143
  const apps: DiscoveredAppLike[] = await runWithRequestContext(
144
144
  { userEmail: verified.email, orgId: localOrg.orgId },
145
145
  async () => {
146
- const discovered = await discoverAgents(SELF_APP_ID);
146
+ const includeDirectoryApp =
147
+ getRequestHeader(event, "x-agent-native-include-directory-app") ===
148
+ "1";
149
+ const discovered = await discoverAgents(
150
+ includeDirectoryApp ? undefined : SELF_APP_ID,
151
+ {
152
+ preferLocalUrls:
153
+ process.env.AGENT_NATIVE_PREFER_LOCAL_APP_URLS === "1",
154
+ },
155
+ );
147
156
  return discovered.map((a) => ({
148
157
  id: a.id,
149
158
  name: a.name,
@@ -165,7 +174,10 @@ const orgAppsHandler = defineEventHandler(
165
174
  const body = buildOrgAppsResponse({
166
175
  org: orgLabel,
167
176
  apps,
168
- selfId: SELF_APP_ID,
177
+ selfId:
178
+ getRequestHeader(event, "x-agent-native-include-directory-app") === "1"
179
+ ? undefined
180
+ : SELF_APP_ID,
169
181
  });
170
182
 
171
183
  // Short, cacheable, read-only. Private (per-org) so shared caches must
@@ -157,6 +157,14 @@ const AddSlideOp = z.object({
157
157
  notes: z.string().optional(),
158
158
  layout: z.string().optional(),
159
159
  background: z.string().optional(),
160
+ imageUrl: z.string().optional(),
161
+ imagePrompt: z.string().optional(),
162
+ excalidrawData: z.string().optional(),
163
+ transition: z
164
+ .enum(["instant", "none", "fade", "slide", "zoom"])
165
+ .optional(),
166
+ animations: z.array(z.unknown()).optional(),
167
+ splitByParagraph: z.boolean().optional(),
160
168
  })
161
169
  .passthrough(),
162
170
  });
@@ -382,7 +390,10 @@ export function applyOperation(deck: any, op: Operation): void {
382
390
  // Idempotency: if the slide already exists (duplicate delivery), skip.
383
391
  if (slides.some((s: { id: string }) => s.id === slideId)) return;
384
392
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
393
+ // Copy every provided field: a duplicated or undo-restored slide has to
394
+ // keep its transition, animations, and image data, not just its text.
385
395
  const newSlide: any = {
396
+ ...fields,
386
397
  id: slideId,
387
398
  content:
388
399
  typeof fields.content === "string"
@@ -391,9 +402,7 @@ export function applyOperation(deck: any, op: Operation): void {
391
402
  notes: fields.notes ?? "",
392
403
  layout: fields.layout ?? "content",
393
404
  };
394
- if (fields.background !== undefined) {
395
- newSlide.background = fields.background;
396
- }
405
+ delete newSlide.imageLoading;
397
406
  const insertAfterIdx = afterSlideId
398
407
  ? slides.findIndex((s: { id: string }) => s.id === afterSlideId)
399
408
  : -1;