@agent-native/core 0.219.0 → 0.220.0-nightly-20261023212848

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 (97) hide show
  1. package/dist/action.d.ts +23 -3
  2. package/dist/action.js +58 -3
  3. package/dist/agent/abort-reasons.d.ts +6 -0
  4. package/dist/agent/abort-reasons.js +6 -0
  5. package/dist/agent/execution-fence.js +5 -1
  6. package/dist/agent/production-agent.d.ts +22 -6
  7. package/dist/agent/production-agent.js +136 -37
  8. package/dist/agent/run-manager.js +92 -44
  9. package/dist/agent/run-store.d.ts +1 -0
  10. package/dist/agent/run-store.js +260 -31
  11. package/dist/agent/run-terminal-tracking.d.ts +29 -0
  12. package/dist/agent/run-terminal-tracking.js +56 -0
  13. package/dist/agent/run-turn-id-header.d.ts +9 -0
  14. package/dist/agent/run-turn-id-header.js +13 -0
  15. package/dist/agent/types.d.ts +11 -0
  16. package/dist/chat-threads/store.d.ts +23 -6
  17. package/dist/chat-threads/store.js +119 -36
  18. package/dist/cli/amplify-stream.js +1 -1
  19. package/dist/client/agent-chat/index.d.ts +2 -1
  20. package/dist/client/agent-chat/index.js +2 -1
  21. package/dist/client/agent-chat.d.ts +2 -0
  22. package/dist/client/agent-chat.js +31 -5
  23. package/dist/client/chat/agentkit-agent-native.js +8 -1
  24. package/dist/client/chat/agentkit-protocol.js +7 -1
  25. package/dist/client/chat/run-outcome-telemetry.js +1 -0
  26. package/dist/client/chat/run-outcome.d.ts +5 -0
  27. package/dist/client/chat/runtime.d.ts +34 -1
  28. package/dist/client/chat/runtime.js +96 -12
  29. package/dist/client/chat/turn-submitted-telemetry.d.ts +4 -0
  30. package/dist/client/chat/turn-submitted-telemetry.js +39 -0
  31. package/dist/client/chat/turn-submitted.d.ts +39 -0
  32. package/dist/client/chat/turn-submitted.js +16 -0
  33. package/dist/client/route-chunk-recovery.d.ts +3 -8
  34. package/dist/client/route-chunk-recovery.js +11 -9
  35. package/dist/client/use-chat-thread-list.d.ts +2 -0
  36. package/dist/client/use-chat-thread-list.js +48 -2
  37. package/dist/client/use-chat-threads.d.ts +13 -0
  38. package/dist/client/use-chat-threads.js +59 -4
  39. package/dist/collab/routes.d.ts +2 -2
  40. package/dist/db/client.d.ts +1 -0
  41. package/dist/db/client.js +28 -18
  42. package/dist/db/schema-snapshot.generated.js +5 -0
  43. package/dist/identity/rekey.d.ts +2 -0
  44. package/dist/identity/rekey.js +19 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/mcp/agent-connected.d.ts +5 -0
  48. package/dist/mcp/agent-connected.js +10 -3
  49. package/dist/mcp/analytics.d.ts +14 -1
  50. package/dist/mcp/analytics.js +54 -2
  51. package/dist/mcp/embed-app.js +2 -1
  52. package/dist/mcp/oauth-route.js +56 -1
  53. package/dist/notifications/routes.d.ts +1 -1
  54. package/dist/observability/routes.d.ts +7 -7
  55. package/dist/observability/traces.d.ts +2 -0
  56. package/dist/observability/traces.js +11 -2
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  58. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  59. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  60. package/dist/resources/handlers.d.ts +1 -1
  61. package/dist/review/index.d.ts +1 -1
  62. package/dist/review/index.js +1 -1
  63. package/dist/review/suggestions/actions.js +25 -17
  64. package/dist/review/suggestions/store.d.ts +14 -1
  65. package/dist/review/suggestions/store.js +16 -3
  66. package/dist/review/suggestions/types.d.ts +15 -1
  67. package/dist/scripts/chat/search-chats.js +14 -2
  68. package/dist/server/agent-chat/browser-team-tools.js +39 -6
  69. package/dist/server/agent-chat/script-entries.js +44 -9
  70. package/dist/server/agent-chat-plugin.d.ts +2 -0
  71. package/dist/server/agent-chat-plugin.js +66 -16
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  74. package/dist/server/first-touch-route.d.ts +2 -2
  75. package/dist/shared/route-chunk-recovery-bootstrap.d.ts +9 -0
  76. package/dist/shared/route-chunk-recovery-bootstrap.js +25 -0
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  79. package/dist/templates/default/.agents/skills/actions/SKILL.md +1 -0
  80. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  81. package/dist/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  82. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  83. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
  84. package/dist/tracking/providers.js +3 -0
  85. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  86. package/docs/content/external-agents.mdx +1 -1
  87. package/docs/content/mcp-apps.mdx +3 -3
  88. package/docs/content/mcp-protocol.mdx +1 -1
  89. package/docs/content/sharing.mdx +1 -0
  90. package/docs/content/template-assets-integrations.mdx +5 -3
  91. package/package.json +3 -3
  92. package/src/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  93. package/src/templates/default/.agents/skills/actions/SKILL.md +1 -0
  94. package/src/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  95. package/src/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  96. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  97. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
@@ -1,4 +1,4 @@
1
- import { renameThread, resolveThreadAccess, setThreadArchived, setThreadPinned, } from "../../chat-threads/store.js";
1
+ import { renameThread, resolveThreadAccess, setThreadArchived, setThreadPinned, setThreadHidden, } from "../../chat-threads/store.js";
2
2
  import { assertCanManageSharedResource } from "../../resources/script-helpers.js";
3
3
  import { dbExecToolParameters } from "../../scripts/db/tool-schemas.js";
4
4
  import { serializeCliArgs } from "../../scripts/parse-args.js";
@@ -515,6 +515,10 @@ export async function createChatScriptEntries() {
515
515
  description: "Output format",
516
516
  enum: ["json", "text"],
517
517
  },
518
+ includeHidden: {
519
+ type: "boolean",
520
+ description: "Also include chats hidden by you. Hidden chats are excluded by default.",
521
+ },
518
522
  includeArchived: {
519
523
  type: "boolean",
520
524
  description: "Also include archived chats in the results. Archived chats are excluded by default.",
@@ -538,14 +542,23 @@ export async function createChatScriptEntries() {
538
542
  return {
539
543
  "chat-history": {
540
544
  tool: {
541
- description: "Manage past agent chat threads. Use action 'search' to find previous conversations by keyword, 'open' to open a thread in the UI, or 'rename'/'pin'/'unpin'/'archive' to organize history.",
545
+ description: "Manage past agent chat threads. Use action 'search' to find previous conversations by keyword, 'open' to open a thread in the UI, or 'pin'/'unpin' to pin for yourself, 'hide'/'unhide' to remove or restore shared chats in your lists. Only owners can rename or archive; owners archive instead of hiding.",
542
546
  parameters: {
543
547
  type: "object",
544
548
  properties: {
545
549
  action: {
546
550
  type: "string",
547
551
  description: "The operation to perform",
548
- enum: ["search", "open", "rename", "pin", "unpin", "archive"],
552
+ enum: [
553
+ "search",
554
+ "open",
555
+ "rename",
556
+ "pin",
557
+ "unpin",
558
+ "archive",
559
+ "hide",
560
+ "unhide",
561
+ ],
549
562
  },
550
563
  query: {
551
564
  type: "string",
@@ -560,13 +573,17 @@ export async function createChatScriptEntries() {
560
573
  description: "(search) Output format",
561
574
  enum: ["json", "text"],
562
575
  },
576
+ includeHidden: {
577
+ type: "boolean",
578
+ description: "(search) Include chats hidden by you, with your hiddenAt timestamp.",
579
+ },
563
580
  includeArchived: {
564
581
  type: "boolean",
565
582
  description: "(search) Also include archived chats in the results. Archived chats are excluded by default, matching the 'archive' action's effect on the chat list.",
566
583
  },
567
584
  id: {
568
585
  type: "string",
569
- description: "(open, rename, pin, unpin, archive) The chat thread ID to manage",
586
+ description: "(open, rename, pin, unpin, archive, hide, unhide) The chat thread ID to manage",
570
587
  },
571
588
  title: {
572
589
  type: "string",
@@ -588,14 +605,16 @@ export async function createChatScriptEntries() {
588
605
  if (args?.action === "rename" ||
589
606
  args?.action === "pin" ||
590
607
  args?.action === "unpin" ||
591
- args?.action === "archive") {
608
+ args?.action === "archive" ||
609
+ args?.action === "hide" ||
610
+ args?.action === "unhide") {
592
611
  const id = typeof args?.id === "string" ? args.id : "";
593
612
  if (!id)
594
613
  return "Missing required id.";
595
614
  const owner = getRequestRunContext()?.owner ?? getRequestUserEmail() ?? "";
596
615
  if (!owner)
597
616
  return "No authenticated user is available.";
598
- const thread = await resolveThreadAccess(owner, id, "editor", {
617
+ const thread = await resolveThreadAccess(owner, id, "viewer", {
599
618
  orgId: getRequestOrgId(),
600
619
  });
601
620
  if (!thread) {
@@ -608,18 +627,34 @@ export async function createChatScriptEntries() {
608
627
  : "";
609
628
  if (!nextTitle)
610
629
  return "Missing required title.";
611
- const renamed = await renameThread(id, nextTitle);
630
+ const renamed = await renameThread(id, nextTitle, {
631
+ ownerEmail: owner,
632
+ });
612
633
  if (!renamed)
613
634
  return `Chat thread "${id}" could not be renamed.`;
614
635
  return `Renamed chat "${title}" to "${nextTitle}".`;
615
636
  }
616
637
  if (args.action === "archive") {
617
- const archived = await setThreadArchived(id, true);
638
+ const archived = await setThreadArchived(id, true, {
639
+ ownerEmail: owner,
640
+ });
618
641
  if (!archived)
619
642
  return `Chat thread "${id}" could not be archived.`;
620
643
  return `Archived chat: ${title}`;
621
644
  }
622
- const pinned = await setThreadPinned(id, args.action === "pin");
645
+ if (args.action === "hide" || args.action === "unhide") {
646
+ const hidden = await setThreadHidden(id, args.action === "hide", {
647
+ ownerEmail: owner,
648
+ orgId: getRequestOrgId(),
649
+ });
650
+ if (!hidden)
651
+ return `Chat thread "${id}" could not be updated.`;
652
+ return `${args.action === "hide" ? "Hidden" : "Unhidden"} chat: ${title}`;
653
+ }
654
+ const pinned = await setThreadPinned(id, args.action === "pin", {
655
+ ownerEmail: owner,
656
+ orgId: getRequestOrgId(),
657
+ });
623
658
  if (!pinned)
624
659
  return `Chat thread "${id}" could not be updated.`;
625
660
  return `${args.action === "pin" ? "Pinned" : "Unpinned"} chat: ${title}`;
@@ -14,6 +14,8 @@ type AgentChatRunTrackingSource = Pick<TrackingMeta, "userId" | "authUserId" | "
14
14
  isSyntheticTraffic?: boolean;
15
15
  };
16
16
  export declare function trackAgentChatRunLifecycle(event: "run_started" | "run_finished" | "run_no_reply", threadId: string | undefined, attemptId: string | undefined, userId?: string, properties?: Record<string, unknown>, appId?: string, trackingSource?: AgentChatRunTrackingSource): void;
17
+ /** `run_started` from every agent-chat entry point, keyed to its turn. */
18
+ export declare function trackAgentChatRunStarted(threadId: string, runId: string, turnId: string, owner: string | undefined, appId: string | undefined): void;
17
19
  import { buildSelectedA2AReceiverContext, createA2AEngineToolSurface, DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKENS, DEFAULT_DELEGATED_MAX_TOOL_RESULT_CHARS, isSelectedA2AReceiver, runA2AAgentLoop, runMCPAgentLoop, assembleA2AFinalResponse, buildPublicAgentA2ASkills } from "./agent-chat/action-filters-a2a.js";
18
20
  import { _agentChatPromptSectionsForTests } from "./agent-chat/framework-prompts.js";
19
21
  import { type AgentChatPluginOptions, type NitroPluginDef } from "./agent-chat/plugin-options.js";
@@ -7,7 +7,7 @@ import { appendA2AArtifactLinks, buildA2ARecoverableArtifactMessage, } from "../
7
7
  import { hasConfiguredA2ASecret, isLoopbackAddress, isTrustedLocalRuntime, } from "../a2a/auth-policy.js";
8
8
  import { sanitizeA2ACorrelationId, sanitizeA2ACorrelationMetadata, } from "../a2a/correlation.js";
9
9
  import { createA2AApproval, a2aTaskLeaseThreadId, A2A_TASK_CLAIM_LOST_ERROR_CODE, updateTaskStatusMessage, } from "../a2a/task-store.js";
10
- import { clientAbortReason } from "../agent/abort-reasons.js";
10
+ import { clientAbortReason, EXECUTION_LEASE_LOST_ABORT_REASON, } from "../agent/abort-reasons.js";
11
11
  import { canUpdateAgentAppModelDefaultSettings, normalizeAgentAppModelDefaultAppId, readAgentAppModelDefaultSettings, resetAgentAppModelDefaultSettings, writeAgentAppModelDefaultSettings, } from "../agent/app-model-defaults.js";
12
12
  import { approvalKeyWithArguments } from "../agent/approval-key.js";
13
13
  import { CHATGPT_SUBSCRIPTION_ENGINE_NAME } from "../agent/chatgpt-subscription-contract.js";
@@ -33,7 +33,7 @@ import { attachToolSearch } from "../agent/tool-search.js";
33
33
  import { getAppConfig } from "../app-config/index.js";
34
34
  import { readAppStateForCurrentTab } from "../application-state/script-helpers.js";
35
35
  import { runChatThreadDataMigrations } from "../chat-threads/migrations.js";
36
- import { InlineAttachmentDataNotPersistableError, QueuedSubmissionAdmissionError, adoptThreadScopeIfUnscoped, blockQueuedSubmissionReceipt, clearQueuedSubmissionInterruptRun, createThread, forkThread, getThread, getQueuedSubmissionReceipt, getQueuedSubmissionReceiptStatus, listQueuedSubmissionReceipts, markQueuedSubmissionSubmitted, queuedSubmissionBehaviorMatches, recordQueuedSubmissionInterruptRun, renewQueuedSubmissionInterruptClaim, registerChatThreadsShareable, retryQueuedSubmissionReceipt, resolveRunThreadScope, resolveThreadAccess, resolveThreadAccessScope, listThreads, searchThreads, renameThread, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, mutateThreadQueuedMessages, setThreadSourceIfMissing, isAppOwnedChatScope, isChatThreadOwnership, threadScopeMismatch, } from "../chat-threads/store.js";
36
+ import { InlineAttachmentDataNotPersistableError, QueuedSubmissionAdmissionError, adoptThreadScopeIfUnscoped, blockQueuedSubmissionReceipt, clearQueuedSubmissionInterruptRun, createThread, forkThread, getThread, getQueuedSubmissionReceipt, getQueuedSubmissionReceiptStatus, listQueuedSubmissionReceipts, markQueuedSubmissionSubmitted, queuedSubmissionBehaviorMatches, recordQueuedSubmissionInterruptRun, renewQueuedSubmissionInterruptClaim, registerChatThreadsShareable, retryQueuedSubmissionReceipt, resolveRunThreadScope, resolveThreadAccess, resolveThreadAccessScope, listThreads, searchThreads, renameThread, assertThreadOwner, createThreadShareLink, getThreadByShareToken, getThreadShareState, revokeThreadShareLink, setThreadArchived, setThreadPinned, setThreadHidden, setThreadScope, updateThreadData, withThreadDataLock, deleteThread, mutateThreadQueuedMessages, setThreadSourceIfMissing, isAppOwnedChatScope, isChatThreadOwnership, threadScopeMismatch, } from "../chat-threads/store.js";
37
37
  import { isCheckpointRestorePath } from "../checkpoints/route-match.js";
38
38
  import { createDbAdminAgentTools } from "../db-admin/agent-tools.js";
39
39
  import { isTransientDatabaseError } from "../db/client.js";
@@ -97,6 +97,10 @@ export function trackAgentChatRunLifecycle(event, threadId, attemptId, userId, p
97
97
  attempt_id: attemptId,
98
98
  }, trackingSource ?? runLifecycleTrackingSource(userId));
99
99
  }
100
+ /** `run_started` from every agent-chat entry point, keyed to its turn. */
101
+ export function trackAgentChatRunStarted(threadId, runId, turnId, owner, appId) {
102
+ trackAgentChatRunLifecycle("run_started", threadId, runId, owner, { turn_id: turnId }, appId);
103
+ }
100
104
  /**
101
105
  * The run's owner is set on the run context only once its system prompt is
102
106
  * built, after `run_started`, and a durable worker has no browser session at
@@ -458,8 +462,8 @@ export async function runA2AAgentLoopWithRunManager(runOptions, pluginOptions, t
458
462
  : undefined;
459
463
  if (reasonCode === "stale_execution_fence" ||
460
464
  reasonCode === A2A_TASK_CLAIM_LOST_ERROR_CODE) {
461
- run.abortReason = "execution_lease_lost";
462
- run.abort.abort("execution_lease_lost");
465
+ run.abortReason = EXECUTION_LEASE_LOST_ABORT_REASON;
466
+ run.abort.abort(EXECUTION_LEASE_LOST_ABORT_REASON);
463
467
  return;
464
468
  }
465
469
  cancellation = abortRunDurably(runId, clientAbortReason(reason)).then((aborted) => ({ status: "fulfilled", aborted }), (error) => ({ status: "rejected", error }));
@@ -3994,7 +3998,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
3994
3998
  },
3995
3999
  onRunPrepared: persistSubmittedUserMessage,
3996
4000
  onRunNotStarted: recordUnstartedTurn,
3997
- onRunStart: async (send, threadId, runId) => {
4001
+ onRunStart: async (send, threadId, runId, turnId) => {
3998
4002
  await recordPreRunGitStatus(threadId);
3999
4003
  _runSendByThread.set(threadId, send);
4000
4004
  const runCtx = ensureRequestRunContext();
@@ -4002,7 +4006,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4002
4006
  runCtx.threadId = threadId;
4003
4007
  runCtx.runId = runId;
4004
4008
  }
4005
- trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
4009
+ trackAgentChatRunStarted(threadId, runId, turnId, runCtx?.owner, options?.appId);
4006
4010
  await runPreAgentTurnAutosave(options?.onAgentTurnStart, runCtx?.chatScope, { threadId, runId });
4007
4011
  },
4008
4012
  onRunComplete: async (run, threadId, trackingSource) => {
@@ -4050,7 +4054,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4050
4054
  },
4051
4055
  onRunPrepared: persistSubmittedUserMessage,
4052
4056
  onRunNotStarted: recordUnstartedTurn,
4053
- onRunStart: async (send, threadId, runId) => {
4057
+ onRunStart: async (send, threadId, runId, turnId) => {
4054
4058
  await recordPreRunGitStatus(threadId);
4055
4059
  _runSendByThread.set(threadId, send);
4056
4060
  const runCtx = ensureRequestRunContext();
@@ -4058,7 +4062,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4058
4062
  runCtx.threadId = threadId;
4059
4063
  runCtx.runId = runId;
4060
4064
  }
4061
- trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
4065
+ trackAgentChatRunStarted(threadId, runId, turnId, runCtx?.owner, options?.appId);
4062
4066
  },
4063
4067
  onRunComplete: async (run, threadId, trackingSource) => {
4064
4068
  if (threadId)
@@ -4237,7 +4241,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4237
4241
  },
4238
4242
  onRunPrepared: persistSubmittedUserMessage,
4239
4243
  onRunNotStarted: recordUnstartedTurn,
4240
- onRunStart: async (send, threadId, runId) => {
4244
+ onRunStart: async (send, threadId, runId, turnId) => {
4241
4245
  await recordPreRunGitStatus(threadId);
4242
4246
  _runSendByThread.set(threadId, send);
4243
4247
  const runCtx = ensureRequestRunContext();
@@ -4245,7 +4249,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4245
4249
  runCtx.threadId = threadId;
4246
4250
  runCtx.runId = runId;
4247
4251
  }
4248
- trackAgentChatRunLifecycle("run_started", threadId, runId, runCtx?.owner, {}, options?.appId);
4252
+ trackAgentChatRunStarted(threadId, runId, turnId, runCtx?.owner, options?.appId);
4249
4253
  await runPreAgentTurnAutosave(options?.onAgentTurnStart, runCtx?.chatScope, { threadId, runId });
4250
4254
  },
4251
4255
  onRunComplete: async (run, threadId, trackingSource) => {
@@ -6029,6 +6033,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6029
6033
  // Merge the incoming snapshot delta over the current SQL copy.
6030
6034
  // Let updateThreadData apply delta markers to each latest
6031
6035
  // revision if its compare-and-swap needs to retry.
6036
+ let titleOverrideChanged = false;
6032
6037
  if (hasThreadData) {
6033
6038
  let incoming;
6034
6039
  try {
@@ -6044,6 +6049,10 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6044
6049
  setResponseStatus(event, 400);
6045
6050
  return { error: "Invalid threadData JSON" };
6046
6051
  }
6052
+ titleOverrideChanged =
6053
+ Object.prototype.hasOwnProperty.call(incoming, "_titleOverride") &&
6054
+ incoming._titleOverride !==
6055
+ existing._titleOverride;
6047
6056
  if (containsInlineAttachmentPayload(incoming)) {
6048
6057
  setResponseStatus(event, 400);
6049
6058
  return {
@@ -6075,9 +6084,21 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6075
6084
  else {
6076
6085
  preserveTitleOverride(existing);
6077
6086
  }
6087
+ // Content saves carry the client's cached title, which can be
6088
+ // stale, and the store keeps the owner's title for a shared
6089
+ // editor. Only an override change or a title-only request is a
6090
+ // rename.
6091
+ if (titleOverrideChanged ||
6092
+ (!hasThreadData &&
6093
+ typeof body.title === "string" &&
6094
+ body.title !== thread.title)) {
6095
+ assertThreadOwner(thread, owner);
6096
+ }
6078
6097
  let updated;
6079
6098
  try {
6080
6099
  updated = await updateThreadData(threadId, newThreadData, nextTitle, nextPreview, newMessageCount, {
6100
+ preserveCurrentTitle: thread.ownerEmail.toLowerCase() !==
6101
+ owner.trim().toLowerCase(),
6081
6102
  preserveCurrentTitleAndPreview: isSnapshotDelta,
6082
6103
  onAnnotationConflict: (conflict) => annotationConflicts.push(conflict),
6083
6104
  });
@@ -6317,7 +6338,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6317
6338
  };
6318
6339
  }
6319
6340
  if (method === "POST" && isThreadSubroute("rename")) {
6320
- const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
6341
+ const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
6321
6342
  if (!thread || !threadMatchesRequestedScope(thread)) {
6322
6343
  setResponseStatus(event, 404);
6323
6344
  return { error: "Thread not found" };
@@ -6330,7 +6351,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6330
6351
  setResponseStatus(event, 400);
6331
6352
  return { error: "Title is required" };
6332
6353
  }
6333
- const renamed = await renameThread(threadId, title);
6354
+ const renamed = await renameThread(threadId, title, {
6355
+ ownerEmail: owner,
6356
+ });
6334
6357
  if (!renamed) {
6335
6358
  setResponseStatus(event, 404);
6336
6359
  return { error: "Thread not found" };
@@ -6338,7 +6361,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6338
6361
  return { ok: true };
6339
6362
  }
6340
6363
  if (method === "POST" && isThreadSubroute("pin")) {
6341
- const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
6364
+ const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
6342
6365
  if (!thread || !threadMatchesRequestedScope(thread)) {
6343
6366
  setResponseStatus(event, 404);
6344
6367
  return { error: "Thread not found" };
@@ -6348,15 +6371,39 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6348
6371
  setResponseStatus(event, 400);
6349
6372
  return { error: "pinned boolean is required" };
6350
6373
  }
6351
- const pinned = await setThreadPinned(threadId, body.pinned);
6374
+ const pinned = await setThreadPinned(threadId, body.pinned, {
6375
+ ownerEmail: owner,
6376
+ orgId,
6377
+ });
6352
6378
  if (!pinned) {
6353
6379
  setResponseStatus(event, 404);
6354
6380
  return { error: "Thread not found" };
6355
6381
  }
6356
6382
  return { ok: true };
6357
6383
  }
6384
+ if (method === "POST" && isThreadSubroute("hide")) {
6385
+ const thread = await resolveThreadAccessScope(owner, threadId, "viewer", { orgId });
6386
+ if (!thread || !threadMatchesRequestedScope(thread)) {
6387
+ setResponseStatus(event, 404);
6388
+ return { error: "Thread not found" };
6389
+ }
6390
+ const body = await readBody(event);
6391
+ if (typeof body?.hidden !== "boolean") {
6392
+ setResponseStatus(event, 400);
6393
+ return { error: "hidden boolean is required" };
6394
+ }
6395
+ const hidden = await setThreadHidden(threadId, body.hidden, {
6396
+ ownerEmail: owner,
6397
+ orgId,
6398
+ });
6399
+ if (!hidden) {
6400
+ setResponseStatus(event, 404);
6401
+ return { error: "Thread not found" };
6402
+ }
6403
+ return { ok: true };
6404
+ }
6358
6405
  if (method === "POST" && isThreadSubroute("archive")) {
6359
- const thread = await resolveThreadAccess(owner, threadId, "editor", { orgId });
6406
+ const thread = await resolveThreadAccess(owner, threadId, "viewer", { orgId });
6360
6407
  if (!thread || !threadMatchesRequestedScope(thread)) {
6361
6408
  setResponseStatus(event, 404);
6362
6409
  return { error: "Thread not found" };
@@ -6366,7 +6413,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6366
6413
  setResponseStatus(event, 400);
6367
6414
  return { error: "archived boolean is required" };
6368
6415
  }
6369
- const archived = await setThreadArchived(threadId, body.archived);
6416
+ const archived = await setThreadArchived(threadId, body.archived, { ownerEmail: owner });
6370
6417
  if (!archived) {
6371
6418
  setResponseStatus(event, 404);
6372
6419
  return { error: "Thread not found" };
@@ -6480,6 +6527,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6480
6527
  const unscopedOnly = String(query.unscoped ?? "") === "1";
6481
6528
  const includeExternal = String(query.includeExternal ?? "") === "1";
6482
6529
  const includeArchived = String(query.includeArchived ?? "") === "1";
6530
+ const includeHidden = String(query.includeHidden ?? "") === "1";
6483
6531
  const ownership = query.ownership === undefined ? "all" : query.ownership;
6484
6532
  if (!isChatThreadOwnership(ownership)) {
6485
6533
  setResponseStatus(event, 400);
@@ -6494,6 +6542,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6494
6542
  orgId,
6495
6543
  includeExternal,
6496
6544
  includeArchived,
6545
+ includeHidden,
6497
6546
  sourceAppId: options?.appId ?? null,
6498
6547
  ownership,
6499
6548
  offset,
@@ -6506,6 +6555,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
6506
6555
  orgId,
6507
6556
  includeExternal,
6508
6557
  includeArchived,
6558
+ includeHidden,
6509
6559
  sourceAppId: options?.appId ?? null,
6510
6560
  ownership,
6511
6561
  });
@@ -67,12 +67,12 @@ export declare function createAgentEngineApiKeyHandler(): import("h3").EventHand
67
67
  code: any;
68
68
  } | {
69
69
  errorCode?: undefined;
70
+ error?: undefined;
70
71
  code?: undefined;
71
72
  ok: boolean;
72
73
  key: string;
73
74
  baseUrlKey?: string;
74
75
  scope: AgentEngineApiKeyScope;
75
76
  defaultModel?: import("./agent-engine-default-model-route.js").SavedKeyDefaultModelOutcome;
76
- error?: undefined;
77
77
  }>>;
78
78
  export {};
@@ -8,8 +8,8 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
8
8
  * resolveEngine's credential detection.
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- ok?: undefined;
12
11
  error: string;
12
+ ok?: undefined;
13
13
  scope?: undefined;
14
14
  } | {
15
15
  ok: boolean;
@@ -17,8 +17,8 @@ export type FirstTouchSyncResult = {
17
17
  * browser's `an_ft` and `an_lt` again, so Safari keeps them their 30 days.
18
18
  */
19
19
  export declare function createFirstTouchHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
- error: string;
21
20
  reason?: undefined;
21
+ error: string;
22
22
  stored?: undefined;
23
23
  firstTouch?: undefined;
24
24
  } | {
@@ -32,8 +32,8 @@ export declare function createFirstTouchHandler(): import("h3").EventHandlerWith
32
32
  reason: "expired";
33
33
  firstTouch?: undefined;
34
34
  } | {
35
- error?: undefined;
36
35
  reason?: undefined;
36
+ error?: undefined;
37
37
  stored: true;
38
38
  firstTouch: FirstTouchAttribution;
39
39
  }>>;
@@ -6,5 +6,14 @@ export declare const CHUNK_RECOVERY_CACHE_BUSTER_PARAM = "__agentNativeChunkReco
6
6
  export declare const CHUNK_RECOVERY_PATH_SUFFIX = "/__agent-native-chunk-recovery";
7
7
  export declare const CHUNK_RECOVERY_ORIGINAL_HASH_PARAM = "__agentNativeRecoveryOriginalHash";
8
8
  export declare const ROUTE_WARMUP_PRELOAD_ATTRIBUTE = "data-agent-native-route-warmup";
9
+ export declare const EXTERNAL_EMBED_WINDOW_KEY = "__AGENT_NATIVE_EXTERNAL_EMBED";
10
+ export declare const STALE_CHUNK_EXHAUSTED_KEY = "__agentNativeStaleChunkRecoveryExhausted";
11
+ /**
12
+ * Fired on `window` the first time a stale chunk could not be recovered by a
13
+ * reload. The shared noise rules drop the raw dynamic-import failure because
14
+ * recovery handles it; this is the one signal that it did not, so a deploy whose
15
+ * chunks are gone for everyone stays visible.
16
+ */
17
+ export declare const STALE_CHUNK_RECOVERY_EXHAUSTED_EVENT = "agent-native:stale-chunk-recovery-exhausted";
9
18
  export declare function isLegacyChunkRecoveryRequest(url: URL): boolean;
10
19
  export declare const ROUTE_CHUNK_RECOVERY_BOOTSTRAP_SCRIPT: string;
@@ -6,6 +6,20 @@ export const CHUNK_RECOVERY_CACHE_BUSTER_PARAM = "__agentNativeChunkRecoveryRequ
6
6
  export const CHUNK_RECOVERY_PATH_SUFFIX = "/__agent-native-chunk-recovery";
7
7
  export const CHUNK_RECOVERY_ORIGINAL_HASH_PARAM = "__agentNativeRecoveryOriginalHash";
8
8
  export const ROUTE_WARMUP_PRELOAD_ATTRIBUTE = "data-agent-native-route-warmup";
9
+ // Set by embedApp's transplant path, which runs the app document inside the MCP
10
+ // host's sandbox frame. There, every recovery URL is on the host's origin, so
11
+ // recovery can only navigate the frame to a page the host blocks.
12
+ export const EXTERNAL_EMBED_WINDOW_KEY = "__AGENT_NATIVE_EXTERNAL_EMBED";
13
+ // Where recovery records that it gave up. The inline bootstrap writes it too, so
14
+ // a failure before the client bundle loads is still reported once it does.
15
+ export const STALE_CHUNK_EXHAUSTED_KEY = "__agentNativeStaleChunkRecoveryExhausted";
16
+ /**
17
+ * Fired on `window` the first time a stale chunk could not be recovered by a
18
+ * reload. The shared noise rules drop the raw dynamic-import failure because
19
+ * recovery handles it; this is the one signal that it did not, so a deploy whose
20
+ * chunks are gone for everyone stays visible.
21
+ */
22
+ export const STALE_CHUNK_RECOVERY_EXHAUSTED_EVENT = "agent-native:stale-chunk-recovery-exhausted";
9
23
  export function isLegacyChunkRecoveryRequest(url) {
10
24
  const values = url.searchParams.getAll(CHUNK_RECOVERY_QUERY_PARAM);
11
25
  return values.length === 1 && values[0] === CHUNK_RECOVERY_QUERY_VALUE;
@@ -18,12 +32,23 @@ const pathSuffix=${JSON.stringify(CHUNK_RECOVERY_PATH_SUFFIX)};
18
32
  const originalHashParam=${JSON.stringify(CHUNK_RECOVERY_ORIGINAL_HASH_PARAM)};
19
33
  const reloadKey=${JSON.stringify(STALE_CHUNK_RELOAD_AT_KEY)};
20
34
  const warmupAttr=${JSON.stringify(ROUTE_WARMUP_PRELOAD_ATTRIBUTE)};
35
+ const externalEmbedKey=${JSON.stringify(EXTERNAL_EMBED_WINDOW_KEY)};
36
+ const exhaustedKey=${JSON.stringify(STALE_CHUNK_EXHAUSTED_KEY)};
37
+ const exhaustedEvent=${JSON.stringify(STALE_CHUNK_RECOVERY_EXHAUSTED_EVENT)};
21
38
  document.addEventListener("error",event=>{
22
39
  const target=event.target;
23
40
  const tag=target?.tagName?.toUpperCase();
24
41
  const failedModuleScript=tag==="SCRIPT"&&target.type?.toLowerCase()==="module";
25
42
  const failedModulePreload=tag==="LINK"&&/(?:^|\\s)modulepreload(?:\\s|$)/i.test(target.getAttribute?.("rel")||target.rel||"")&&!target.hasAttribute?.(warmupAttr);
26
43
  if(!failedModuleScript&&!failedModulePreload||/AgentNativeDesktop/i.test(navigator.userAgent))return;
44
+ if(window[externalEmbedKey]){
45
+ if(!window[exhaustedKey]){
46
+ const detail={reason:"external-embed"};
47
+ window[exhaustedKey]=detail;
48
+ if(typeof window.dispatchEvent==="function"&&typeof CustomEvent==="function")window.dispatchEvent(new CustomEvent(exhaustedEvent,{detail}));
49
+ }
50
+ return;
51
+ }
27
52
  if(/^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(location.hostname))return;
28
53
  const url=new URL(location.href);
29
54
  if(url.pathname.endsWith(pathSuffix)||url.pathname.endsWith(pathSuffix+"/")||url.searchParams.get(queryParam)===queryValue)return;
@@ -2,6 +2,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
2
2
  resourceType: string;
3
3
  resourceId: string;
4
4
  }, {
5
+ role?: undefined;
5
6
  ownerEmail: any;
6
7
  visibility: any;
7
8
  shares: any[];
@@ -11,7 +12,6 @@ declare const _default: import("../../action.js").ActionDefinition<{
11
12
  supportsGroupShares?: boolean;
12
13
  };
13
14
  orgId?: undefined;
14
- role?: undefined;
15
15
  agentReadable?: undefined;
16
16
  } | {
17
17
  ownerEmail: string;
@@ -172,6 +172,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
172
172
  - **Do** signal failure by throwing, using `fail()` for expected caller-readable failures. Never return `{ error: ... }` as a failure result: a normal return is treated as a successful action. Reserve bare errors for unexpected internal faults.
173
173
  - **Do** use `fail()` rather than a bare `Error` for caller-readable failures. It carries the action's message, `errorCode`, and `details` to the browser and to agents; unexpected errors become a generic 500. Reserve bare throws for internal faults.
174
174
  - **Do** give `fail()` the status that matches the cause: it defaults to `400`, and `useActionQuery` retries only `429`, `502`, `503`, and `504`. Mutations do not retry; use transient statuses only for retryable failures.
175
+ - **Do** throw `NoWriteError(message, { errorCode, statusCode })` instead of `fail()` for a rejection raised before the action's first write, such as an edit that matches nothing or a missing target. The agent then retries with corrected arguments instead of stopping on a write whose outcome is unknown. Throw it only before the first write: the framework rejects the claim when a nested write action ran, but it cannot see the handler's own writes.
175
176
  - **Do** render `actionErrorMessage(error) ?? yourCopy` in UI, never bare `error.message`. The message keeps an `Action <name> failed:` prefix that belongs in a console, so a toast built from it reads "Action update-brand-kit failed: That name is taken." The helper returns only what the action wrote, and `undefined` when nothing did (network drop, proxy HTML page), which is why the fallback is not optional.
176
177
  - **Do** pass a real `errorCode` when the agent should branch on the failure rather than re-read it. Codes other than the default `action_failed` are appended to the tool result as `(errorCode: not_found)`, on both the in-app agent and MCP. `details` follow as an `Error details: {...}` JSON line, and as `structuredContent.error` over MCP, capped at 2,000 characters, so keep them to what the agent acts on next: ids, limits, field names. The status never reaches either.
177
178
  - **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
@@ -172,6 +172,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
172
172
  - **Do** signal failure by throwing, using `fail()` for expected caller-readable failures. Never return `{ error: ... }` as a failure result: a normal return is treated as a successful action. Reserve bare errors for unexpected internal faults.
173
173
  - **Do** use `fail()` rather than a bare `Error` for caller-readable failures. It carries the action's message, `errorCode`, and `details` to the browser and to agents; unexpected errors become a generic 500. Reserve bare throws for internal faults.
174
174
  - **Do** give `fail()` the status that matches the cause: it defaults to `400`, and `useActionQuery` retries only `429`, `502`, `503`, and `504`. Mutations do not retry; use transient statuses only for retryable failures.
175
+ - **Do** throw `NoWriteError(message, { errorCode, statusCode })` instead of `fail()` for a rejection raised before the action's first write, such as an edit that matches nothing or a missing target. The agent then retries with corrected arguments instead of stopping on a write whose outcome is unknown. Throw it only before the first write: the framework rejects the claim when a nested write action ran, but it cannot see the handler's own writes.
175
176
  - **Do** render `actionErrorMessage(error) ?? yourCopy` in UI, never bare `error.message`. The message keeps an `Action <name> failed:` prefix that belongs in a console, so a toast built from it reads "Action update-brand-kit failed: That name is taken." The helper returns only what the action wrote, and `undefined` when nothing did (network drop, proxy HTML page), which is why the fallback is not optional.
176
177
  - **Do** pass a real `errorCode` when the agent should branch on the failure rather than re-read it. Codes other than the default `action_failed` are appended to the tool result as `(errorCode: not_found)`, on both the in-app agent and MCP. `details` follow as an `Error details: {...}` JSON line, and as `structuredContent.error` over MCP, capped at 2,000 characters, so keep them to what the agent acts on next: ids, limits, field names. The status never reaches either.
177
178
  - **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
@@ -287,6 +287,12 @@ not already settled by Steve's written direction, flag it to Steve and do not
287
287
  merge until he decides. A routine bug fix or bounded improvement within an
288
288
  established workflow is not a major decision.
289
289
 
290
+ Keep the PR thread silent while a product, UX, or app-owner decision is
291
+ unresolved. Do not comment or reply to request, announce, or explain that the
292
+ decision is needed; record the gate only in the recap. This remains true after
293
+ an author update, so do not send a thanks-only or status-only reply while the
294
+ decision is pending.
295
+
290
296
  The app-owner map is for approval exceptions only; an owner approval is not a
291
297
  merge requirement:
292
298
 
@@ -455,12 +461,29 @@ code or screenshot requests. This also applies when another comment or bot
455
461
  event is newer than Steve's request; bot activity alone does not reopen the
456
462
  handoff.
457
463
 
464
+ On a contributor-owned PR, do not draft a follow-up unless the author has made
465
+ a substantive update since Steve's last message. Begin a first reply by
466
+ thanking the author for the PR; if replying after an author update, begin by
467
+ thanking them for that update. Never write “I have no further feedback” or
468
+ another vague closing. State the disposition, the exact next action, and who
469
+ owns it. If a product, UX, or app-owner decision is still needed, do not post
470
+ an author-facing status comment or ask the author to get that review; keep the
471
+ thread silent and report the decision gate in the recap. Never comment that a
472
+ product owner needs to review the PR. For a contributor thread paused by an
473
+ unresolved gate, an internal decision does not count as an author update, and
474
+ an author update made while the decision is pending does not qualify for a
475
+ follow-up. Keep the thread silent until the decision is resolved and the author
476
+ makes a substantive update after resolution; then thank them for that update
477
+ and state the resolved disposition, exact next action, and owner. This
478
+ contributor-update gate does not govern review feedback on the active user's
479
+ own PR; follow the own-PR review-update rule in `ship`.
480
+
458
481
  If no prior Steve request is awaiting an update and this would be Steve's first
459
482
  comment on that PR, begin the draft with a warm thank-you that tags the author
460
483
  using the exact GitHub `author.login` from the live PR metadata, for example:
461
484
 
462
485
  ```text
463
- Thanks for the contribution @username!
486
+ Thanks for the PR, @username!
464
487
 
465
488
  Couple requests. Can you please [make the specific update] so [the intended
466
489
  behavior or outcome] is preserved?
@@ -473,8 +496,9 @@ the concrete request in a friendly, conversational way (for example, "Couple
473
496
  requests. Can you please ...?"). Keep the technical request specific,
474
497
  actionable, and supported by the diff or source evidence; ask for a regression
475
498
  test when appropriate to the code change rather than as a blanket requirement.
476
- Do not repeat a thank-you on a follow-up. Do not draft a duplicate request when
477
- an existing Steve comment already covers it; link or summarize that request
499
+ For a follow-up after an author update, thank them for that update rather than
500
+ repeating the first-contact thanks. Do not draft a duplicate request when an
501
+ existing Steve comment already covers it; link or summarize that request
478
502
  instead. Drafts are for the user to review and are never posted by this skill
479
503
  unless the current invocation explicitly authorizes posting.
480
504
 
@@ -172,6 +172,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
172
172
  - **Do** signal failure by throwing, using `fail()` for expected caller-readable failures. Never return `{ error: ... }` as a failure result: a normal return is treated as a successful action. Reserve bare errors for unexpected internal faults.
173
173
  - **Do** use `fail()` rather than a bare `Error` for caller-readable failures. It carries the action's message, `errorCode`, and `details` to the browser and to agents; unexpected errors become a generic 500. Reserve bare throws for internal faults.
174
174
  - **Do** give `fail()` the status that matches the cause: it defaults to `400`, and `useActionQuery` retries only `429`, `502`, `503`, and `504`. Mutations do not retry; use transient statuses only for retryable failures.
175
+ - **Do** throw `NoWriteError(message, { errorCode, statusCode })` instead of `fail()` for a rejection raised before the action's first write, such as an edit that matches nothing or a missing target. The agent then retries with corrected arguments instead of stopping on a write whose outcome is unknown. Throw it only before the first write: the framework rejects the claim when a nested write action ran, but it cannot see the handler's own writes.
175
176
  - **Do** render `actionErrorMessage(error) ?? yourCopy` in UI, never bare `error.message`. The message keeps an `Action <name> failed:` prefix that belongs in a console, so a toast built from it reads "Action update-brand-kit failed: That name is taken." The helper returns only what the action wrote, and `undefined` when nothing did (network drop, proxy HTML page), which is why the fallback is not optional.
176
177
  - **Do** pass a real `errorCode` when the agent should branch on the failure rather than re-read it. Codes other than the default `action_failed` are appended to the tool result as `(errorCode: not_found)`, on both the in-app agent and MCP. `details` follow as an `Error details: {...}` JSON line, and as `structuredContent.error` over MCP, capped at 2,000 characters, so keep them to what the agent acts on next: ids, limits, field names. The status never reaches either.
177
178
  - **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
@@ -172,6 +172,7 @@ Everything else — CRUD, settings, search, list/detail reads, auth state, anyth
172
172
  - **Do** signal failure by throwing, using `fail()` for expected caller-readable failures. Never return `{ error: ... }` as a failure result: a normal return is treated as a successful action. Reserve bare errors for unexpected internal faults.
173
173
  - **Do** use `fail()` rather than a bare `Error` for caller-readable failures. It carries the action's message, `errorCode`, and `details` to the browser and to agents; unexpected errors become a generic 500. Reserve bare throws for internal faults.
174
174
  - **Do** give `fail()` the status that matches the cause: it defaults to `400`, and `useActionQuery` retries only `429`, `502`, `503`, and `504`. Mutations do not retry; use transient statuses only for retryable failures.
175
+ - **Do** throw `NoWriteError(message, { errorCode, statusCode })` instead of `fail()` for a rejection raised before the action's first write, such as an edit that matches nothing or a missing target. The agent then retries with corrected arguments instead of stopping on a write whose outcome is unknown. Throw it only before the first write: the framework rejects the claim when a nested write action ran, but it cannot see the handler's own writes.
175
176
  - **Do** render `actionErrorMessage(error) ?? yourCopy` in UI, never bare `error.message`. The message keeps an `Action <name> failed:` prefix that belongs in a console, so a toast built from it reads "Action update-brand-kit failed: That name is taken." The helper returns only what the action wrote, and `undefined` when nothing did (network drop, proxy HTML page), which is why the fallback is not optional.
176
177
  - **Do** pass a real `errorCode` when the agent should branch on the failure rather than re-read it. Codes other than the default `action_failed` are appended to the tool result as `(errorCode: not_found)`, on both the in-app agent and MCP. `details` follow as an `Error details: {...}` JSON line, and as `structuredContent.error` over MCP, capped at 2,000 characters, so keep them to what the agent acts on next: ids, limits, field names. The status never reaches either.
177
178
  - **Don't** re-export actions as REST — `/_agent-native/actions/:name` is already the REST surface; duplicating it under `/api/*` hides the operation from agents.
@@ -162,8 +162,15 @@ actions for Forms, Content, Clips, Slides, and Design. The action should return
162
162
  concise structured content plus the link; it should not dump large catalogs or
163
163
  HTML.
164
164
 
165
- Compatibility target: build to the standard once, not per-client shims. MCP
166
- Apps-capable hosts should include Claude/Claude Desktop/Claude Code, ChatGPT
167
- custom MCP apps, VS Code GitHub Copilot, Goose, Postman, MCPJam, Cursor, and
168
- any future host that follows the extension negotiation. Host support varies by
169
- plan, release channel, and client version, so keep the deep link fallback.
165
+ Compatibility target: build to the standard once, not per-client shims.
166
+ Hosts that render MCP Apps inline today include Claude chat on web, desktop, and
167
+ mobile, plus the Claude Desktop Code tab; ChatGPT; VS Code GitHub Copilot (1.109+);
168
+ Microsoft 365 Copilot; Cursor (2.6+); Goose; Postman; MCPJam; Codex Desktop (26.616+);
169
+ and T3 Code (in Codex threads). Desktop Code rendering is observed in
170
+ [widget bug reports](https://github.com/anthropics/claude-code/issues/103809).
171
+ Treat the Claude Code terminal, Cowork, and Code on claude.ai web as link-out hosts.
172
+ [Cowork doesn't declare MCP Apps yet](https://github.com/anthropics/claude-code/issues/103924),
173
+ and using it on the same connector can make inline apps disappear in Chat until
174
+ you reconnect the connector. Host support varies by plan, release channel, and client version,
175
+ so keep the deep link fallback. See the community-maintained
176
+ [MCP client matrix](https://modelcontextprotocol.io/extensions/client-matrix).