@agent-native/core 0.227.0 → 0.228.0-nightly-20261111004142

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 (190) hide show
  1. package/dist/action.d.ts +6 -2
  2. package/dist/agent/engine/builder-engine.js +6 -1
  3. package/dist/agent/engine/tool-call-journal-seed.d.ts +2 -0
  4. package/dist/agent/engine/tool-call-journal-seed.js +14 -0
  5. package/dist/agent/harness/store.js +14 -0
  6. package/dist/agent/production-agent.js +257 -17
  7. package/dist/agent/reaped-run-thread.js +6 -0
  8. package/dist/agent/run-manager.js +6 -0
  9. package/dist/agent/tool-call-journal.d.ts +4 -0
  10. package/dist/agent/tool-call-journal.js +12 -0
  11. package/dist/agent/types.d.ts +7 -1
  12. package/dist/app-config/agent.d.ts +3 -0
  13. package/dist/app-config/agent.js +7 -0
  14. package/dist/app-config/schema.d.ts +3 -0
  15. package/dist/application-state/handlers.d.ts +3 -3
  16. package/dist/application-state/handlers.js +6 -1
  17. package/dist/application-state/index.d.ts +3 -2
  18. package/dist/application-state/index.js +3 -2
  19. package/dist/application-state/last-used-tab.d.ts +11 -0
  20. package/dist/application-state/last-used-tab.js +43 -0
  21. package/dist/application-state/script-helpers.d.ts +26 -0
  22. package/dist/application-state/script-helpers.js +83 -15
  23. package/dist/application-state/store.d.ts +8 -0
  24. package/dist/application-state/store.js +16 -0
  25. package/dist/cli/agent.js +7 -3
  26. package/dist/cli/code-agent-executor.d.ts +1 -0
  27. package/dist/cli/code-agent-executor.js +37 -2
  28. package/dist/cli/code.js +3 -1
  29. package/dist/client/agent-sidebar-state.d.ts +2 -0
  30. package/dist/client/browser-tab-use.d.ts +9 -0
  31. package/dist/client/browser-tab-use.js +112 -0
  32. package/dist/client/chat/runtime.js +3 -1
  33. package/dist/client/error-capture.js +33 -1
  34. package/dist/client/navigation/index.d.ts +1 -0
  35. package/dist/client/navigation/index.js +1 -0
  36. package/dist/client/resources/mcp-integration-logos.js +16 -0
  37. package/dist/client/review/use-review.d.ts +28 -0
  38. package/dist/client/review/use-review.js +41 -12
  39. package/dist/client/route-state.js +2 -0
  40. package/dist/client/sse-event-processor.d.ts +1 -0
  41. package/dist/client/sse-event-processor.js +7 -1
  42. package/dist/client/use-db-sync.d.ts +8 -1
  43. package/dist/client/use-db-sync.js +6 -5
  44. package/dist/coding-tools/run-code.js +12 -2
  45. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -1
  46. package/dist/coding-tools/sandbox/executions-store.js +9 -3
  47. package/dist/collab/routes.d.ts +1 -1
  48. package/dist/data-programs/execute.js +1 -1
  49. package/dist/db/schema-snapshot.generated.js +17 -3
  50. package/dist/extensions/url-safety.d.ts +1 -3
  51. package/dist/extensions/url-safety.js +23 -23
  52. package/dist/file-upload/actions/upload-image.d.ts +5 -5
  53. package/dist/file-upload/index.d.ts +1 -1
  54. package/dist/file-upload/index.js +1 -1
  55. package/dist/file-upload/s3-client.d.ts +1 -0
  56. package/dist/file-upload/s3-client.js +70 -13
  57. package/dist/file-upload/s3.d.ts +9 -0
  58. package/dist/file-upload/s3.js +14 -1
  59. package/dist/framework-package-versions.json +8 -8
  60. package/dist/jobs/run-history.js +10 -3
  61. package/dist/jobs/scheduler.js +5 -36
  62. package/dist/localization/core-messages/supplemental/ar-SA.js +35 -0
  63. package/dist/localization/core-messages/supplemental/de-DE.js +35 -0
  64. package/dist/localization/core-messages/supplemental/en-US.js +35 -0
  65. package/dist/localization/core-messages/supplemental/es-ES.js +35 -0
  66. package/dist/localization/core-messages/supplemental/fr-FR.js +35 -0
  67. package/dist/localization/core-messages/supplemental/hi-IN.js +35 -0
  68. package/dist/localization/core-messages/supplemental/ja-JP.js +35 -0
  69. package/dist/localization/core-messages/supplemental/ko-KR.js +35 -0
  70. package/dist/localization/core-messages/supplemental/pt-BR.js +35 -0
  71. package/dist/localization/core-messages/supplemental/zh-CN.js +35 -0
  72. package/dist/localization/core-messages/supplemental/zh-TW.js +35 -0
  73. package/dist/localization/default-messages.d.ts +35 -0
  74. package/dist/localization/default-messages.js +35 -0
  75. package/dist/localization/mcp-settings-messages.d.ts +35 -0
  76. package/dist/mcp/actions/list-mcp-client-connections.d.ts +8 -0
  77. package/dist/mcp/actions/list-mcp-client-connections.js +39 -0
  78. package/dist/mcp/browser-tab-target.d.ts +16 -0
  79. package/dist/mcp/browser-tab-target.js +58 -0
  80. package/dist/mcp/build-server.d.ts +1 -0
  81. package/dist/mcp/build-server.js +38 -7
  82. package/dist/mcp/call-attribution.d.ts +4 -1
  83. package/dist/mcp/call-attribution.js +3 -1
  84. package/dist/mcp/client-connections.d.ts +6 -0
  85. package/dist/mcp/client-connections.js +91 -0
  86. package/dist/mcp/connect-route.js +12 -2
  87. package/dist/mcp/connect-store.d.ts +3 -0
  88. package/dist/mcp/connect-store.js +10 -1
  89. package/dist/mcp/direct-action-call.js +1 -0
  90. package/dist/mcp/oauth-store.d.ts +6 -0
  91. package/dist/mcp/oauth-store.js +23 -0
  92. package/dist/mcp/oauth-token.d.ts +1 -0
  93. package/dist/mcp/oauth-token.js +1 -0
  94. package/dist/mcp/server.js +9 -20
  95. package/dist/mcp-client/config.d.ts +2 -0
  96. package/dist/mcp-client/manager.js +23 -0
  97. package/dist/mcp-client/workspace-servers.js +42 -10
  98. package/dist/notifications/routes.d.ts +3 -3
  99. package/dist/org/handlers.d.ts +1 -1
  100. package/dist/org/handlers.js +21 -17
  101. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  102. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  103. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  104. package/dist/provider-api/index.js +17 -36
  105. package/dist/review/actions/create-review-comment.d.ts +7 -0
  106. package/dist/review/actions/create-review-comment.js +29 -3
  107. package/dist/review/actions/delete-review-comment.js +2 -2
  108. package/dist/review/actions/list-review-comments.d.ts +3 -0
  109. package/dist/review/actions/list-review-comments.js +41 -17
  110. package/dist/review/actions/react-to-review-comment.js +2 -2
  111. package/dist/review/actions/reply-review-comment.d.ts +4 -0
  112. package/dist/review/actions/reply-review-comment.js +15 -6
  113. package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
  114. package/dist/review/actions/update-review-comment-anchor.js +39 -10
  115. package/dist/review/actions/update-review-comment.d.ts +3 -0
  116. package/dist/review/actions/update-review-comment.js +28 -3
  117. package/dist/review/errors.d.ts +33 -0
  118. package/dist/review/errors.js +51 -0
  119. package/dist/review/index.d.ts +5 -4
  120. package/dist/review/index.js +3 -2
  121. package/dist/review/registry.d.ts +51 -1
  122. package/dist/review/registry.js +117 -0
  123. package/dist/review/store.d.ts +59 -14
  124. package/dist/review/store.js +622 -101
  125. package/dist/review/suggestions/actions.js +65 -10
  126. package/dist/review/suggestions/actor-kind.d.ts +5 -0
  127. package/dist/review/suggestions/actor-kind.js +20 -0
  128. package/dist/review/suggestions/store.js +16 -2
  129. package/dist/review/suggestions/types.d.ts +13 -0
  130. package/dist/review/types.d.ts +89 -0
  131. package/dist/scripts/db/dev-query-proxy.d.ts +2 -0
  132. package/dist/scripts/db/dev-query-proxy.js +4 -1
  133. package/dist/scripts/db/query.js +3 -0
  134. package/dist/scripts/dev/shell.js +4 -0
  135. package/dist/scripts/dev-session.d.ts +31 -0
  136. package/dist/scripts/dev-session.js +58 -0
  137. package/dist/scripts/resources/list.js +53 -4
  138. package/dist/scripts/runner.js +22 -4
  139. package/dist/search/query.d.ts +11 -5
  140. package/dist/search/query.js +52 -15
  141. package/dist/search/tokenize.d.ts +1 -0
  142. package/dist/search/tokenize.js +3 -0
  143. package/dist/secrets/api-keys.d.ts +4 -0
  144. package/dist/secrets/api-keys.js +5 -0
  145. package/dist/secrets/register.d.ts +2 -0
  146. package/dist/secrets/routes.d.ts +2 -2
  147. package/dist/secrets/routes.js +85 -6
  148. package/dist/secrets/storage.d.ts +2 -0
  149. package/dist/secrets/storage.js +34 -9
  150. package/dist/secrets/substitution.d.ts +3 -2
  151. package/dist/secrets/substitution.js +56 -7
  152. package/dist/server/action-discovery.js +5 -0
  153. package/dist/server/action-routes.js +8 -6
  154. package/dist/server/agent-chat/prompt-resources.js +117 -79
  155. package/dist/server/agent-chat/script-entries.js +15 -8
  156. package/dist/server/agent-chat-plugin.js +9 -4
  157. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  158. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  159. package/dist/server/agents-bundle.d.ts +3 -1
  160. package/dist/server/agents-bundle.js +4 -4
  161. package/dist/server/csrf.js +1 -0
  162. package/dist/server/dev-action-bridge.d.ts +2 -0
  163. package/dist/server/dev-action-bridge.js +34 -5
  164. package/dist/server/first-touch-route.d.ts +1 -1
  165. package/dist/server/http-response-telemetry.js +4 -0
  166. package/dist/server/loopback.js +3 -1
  167. package/dist/server/request-context.d.ts +26 -0
  168. package/dist/shared/app-state-routes.d.ts +7 -0
  169. package/dist/shared/app-state-routes.js +7 -0
  170. package/dist/shared/mcp-client-connections.d.ts +41 -0
  171. package/dist/shared/mcp-client-connections.js +18 -0
  172. package/dist/shared/mcp-connect-content.d.ts +18 -0
  173. package/dist/shared/mcp-connect-content.js +200 -172
  174. package/dist/shared/runtime-config.js +5 -1
  175. package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  176. package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  177. package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
  178. package/dist/tracking/providers.d.ts +7 -0
  179. package/dist/tracking/providers.js +39 -17
  180. package/dist/triggers/condition-evaluator.d.ts +2 -0
  181. package/dist/triggers/condition-evaluator.js +93 -3
  182. package/dist/triggers/dispatcher.js +24 -6
  183. package/docs/AGENTS.md +16 -16
  184. package/docs/content/create-with-an-agent.mdx +10 -4
  185. package/docs/content/durable-resume.mdx +11 -2
  186. package/docs/content/toolkit-comments-review.mdx +54 -1
  187. package/package.json +5 -4
  188. package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  189. package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  190. package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
@@ -2,9 +2,9 @@ import { z } from "zod";
2
2
  import { defineAction, fail } from "../../action.js";
3
3
  import { getDbExec } from "../../db/client.js";
4
4
  import { notifyReviewComment } from "../notifications.js";
5
- import { assertReviewableResourceAccess } from "../registry.js";
5
+ import { assertReviewableResourceAccess, REVIEW_SUGGESTION_ANCHOR_KIND, reviewSuggestionAuthorRole, } from "../registry.js";
6
6
  import { ensureReviewTables, insertReviewCommentWithClient, resolveReviewThreadWithClient, } from "../store.js";
7
- import { suggestionActorKind, suggestionActorKindMatchesReceipt, } from "./actor-kind.js";
7
+ import { suggestionActorKind, suggestionActorKindMatchesReceipt, suggestionAuthorClient, suggestionAuthorSource, } from "./actor-kind.js";
8
8
  import { getSuggestionAdapter } from "./registry.js";
9
9
  import { getSuggestion, getSuggestions, ensureSuggestionTables, insertSuggestion, listSuggestions, recordDecision, getDecision, getSuggestionByCreationKey, recordSuggestionCreation, updateSuggestionStatus, getSuggestionAmendment, amendSuggestion, deleteUnclaimedSuggestion, getSuggestionProposal, insertSuggestionProposal, getProposalCreation, recordProposalCreation, getProposalDecision, recordProposalDecision, } from "./store.js";
10
10
  const base = { resourceType: z.string().min(1), resourceId: z.string().min(1) };
@@ -108,6 +108,20 @@ async function supersedeSuggestions(tx, args, authorEmail, replacementId) {
108
108
  await resolveReviewThreadWithClient(tx, prior.threadId, authorEmail, { resourceType: prior.resourceType, resourceId: prior.resourceId }, "superseded");
109
109
  }
110
110
  }
111
+ // So a suggestion's change event reaches everyone who can read the resource,
112
+ // not only the actor: their open page shows it without a reload.
113
+ function suggestionChangeResource(suggestion) {
114
+ return suggestion
115
+ ? {
116
+ resourceType: suggestion.resourceType,
117
+ resourceId: suggestion.resourceId,
118
+ }
119
+ : null;
120
+ }
121
+ // A failed call reaches changeResource with no result, whatever its type says.
122
+ function decisionChangeResource(result) {
123
+ return suggestionChangeResource(result?.suggestion ?? result?.suggestions?.[0]);
124
+ }
111
125
  export const createResourceSuggestion = defineAction({
112
126
  description: "Create a typed pending suggestion without changing the canonical resource. Pass supersedes to replace your own earlier pending suggestions on the same resource in the same step.",
113
127
  schema: z.object({
@@ -135,8 +149,12 @@ export const createResourceSuggestion = defineAction({
135
149
  openWorldHint: true,
136
150
  idempotentHint: true,
137
151
  },
152
+ changeResource: ({ resourceType, resourceId }) => ({
153
+ resourceType,
154
+ resourceId,
155
+ }),
138
156
  run: async (args, ctx) => {
139
- const access = await assertReviewableResourceAccess(args.resourceType, args.resourceId, ctx, "commenter");
157
+ const access = await assertReviewableResourceAccess(args.resourceType, args.resourceId, ctx, reviewSuggestionAuthorRole(args.resourceType));
140
158
  const adapter = getSuggestionAdapter(args.adapterKind);
141
159
  if (!adapter)
142
160
  throw new Error("Suggestion adapter not registered");
@@ -172,6 +190,8 @@ export const createResourceSuggestion = defineAction({
172
190
  threadId: `suggestion-thread-${globalThis.crypto.randomUUID()}`,
173
191
  authorEmail,
174
192
  actorKind,
193
+ authorSource: suggestionAuthorSource(ctx),
194
+ authorClient: suggestionAuthorClient(ctx),
175
195
  baseRevision: args.baseRevision,
176
196
  status: "pending",
177
197
  summary: args.summary,
@@ -197,6 +217,7 @@ export const createResourceSuggestion = defineAction({
197
217
  targetId: created.id,
198
218
  kind: "correction",
199
219
  anchor: created.operations.map((item) => item.anchor ?? null),
220
+ anchorKind: REVIEW_SUGGESTION_ANCHOR_KIND,
200
221
  body: created.summary,
201
222
  authorEmail: created.authorEmail,
202
223
  createdBy: actorKind,
@@ -261,7 +282,7 @@ export const updateResourceSuggestion = defineAction({
261
282
  const initial = await getSuggestion(args.id);
262
283
  if (!initial)
263
284
  fail("Suggestion not found", { statusCode: 404, errorCode: "not_found" });
264
- await assertReviewableResourceAccess(initial.resourceType, initial.resourceId, ctx, "commenter");
285
+ await assertReviewableResourceAccess(initial.resourceType, initial.resourceId, ctx, reviewSuggestionAuthorRole(initial.resourceType));
265
286
  const author = ctx?.userEmail;
266
287
  if (!author || author !== initial.authorEmail) {
267
288
  fail("Only the author can amend this suggestion", {
@@ -285,7 +306,7 @@ export const updateResourceSuggestion = defineAction({
285
306
  statusCode: 404,
286
307
  errorCode: "not_found",
287
308
  });
288
- const access = await assertReviewableResourceAccess(current.resourceType, current.resourceId, { ...ctx, transaction: tx }, "commenter");
309
+ const access = await assertReviewableResourceAccess(current.resourceType, current.resourceId, { ...ctx, transaction: tx }, reviewSuggestionAuthorRole(current.resourceType));
289
310
  if (current.authorEmail !== author)
290
311
  fail("Only the author can amend this suggestion", {
291
312
  statusCode: 403,
@@ -339,6 +360,7 @@ export const updateResourceSuggestion = defineAction({
339
360
  };
340
361
  },
341
362
  },
363
+ changeResource: (_input, result) => suggestionChangeResource(result),
342
364
  });
343
365
  export const listResourceSuggestions = defineAction({
344
366
  description: "List typed suggestions for a resource.",
@@ -394,7 +416,7 @@ function decisionAccessRole(suggestion, decision, ctx) {
394
416
  errorCode: "forbidden",
395
417
  });
396
418
  }
397
- return "commenter";
419
+ return reviewSuggestionAuthorRole(suggestion.resourceType);
398
420
  }
399
421
  export const decideResourceSuggestion = defineAction({
400
422
  description: "Accept or reject a pending suggestion atomically, which needs edit access. An accept that can no longer be placed on the current resource fails with 409 `suggestion_stale`, changes nothing, and leaves the suggestion pending. Its author may instead withdraw it with comment access; a withdrawn suggestion was never reviewed and leaves the resource unchanged.",
@@ -551,6 +573,7 @@ export const decideResourceSuggestion = defineAction({
551
573
  : undefined;
552
574
  },
553
575
  },
576
+ changeResource: (_input, result) => decisionChangeResource(result),
554
577
  });
555
578
  const proposalCreationSchema = z.object({
556
579
  ...base,
@@ -611,8 +634,12 @@ export const createResourceSuggestionProposal = defineAction({
611
634
  openWorldHint: true,
612
635
  idempotentHint: true,
613
636
  },
637
+ changeResource: ({ resourceType, resourceId }) => ({
638
+ resourceType,
639
+ resourceId,
640
+ }),
614
641
  run: async (args, ctx) => {
615
- await assertReviewableResourceAccess(args.resourceType, args.resourceId, ctx, "commenter");
642
+ await assertReviewableResourceAccess(args.resourceType, args.resourceId, ctx, reviewSuggestionAuthorRole(args.resourceType));
616
643
  const adapter = getSuggestionAdapter(args.adapterKind);
617
644
  if (!adapter)
618
645
  throw new Error("Suggestion adapter not registered");
@@ -634,7 +661,7 @@ export const createResourceSuggestionProposal = defineAction({
634
661
  if (!db.transaction)
635
662
  throw new Error("Proposal creation requires an atomic database transaction");
636
663
  const replay = async (tx, prior) => {
637
- await assertReviewableResourceAccess(args.resourceType, args.resourceId, { ...ctx, transaction: tx }, "commenter");
664
+ await assertReviewableResourceAccess(args.resourceType, args.resourceId, { ...ctx, transaction: tx }, reviewSuggestionAuthorRole(args.resourceType));
638
665
  if (prior.requestHash !== requestHash ||
639
666
  prior.authorEmail !== authorEmail ||
640
667
  prior.actorKind !== actorKind) {
@@ -664,7 +691,7 @@ export const createResourceSuggestionProposal = defineAction({
664
691
  const prior = await getProposalCreation(tx, args.idempotencyKey);
665
692
  if (prior)
666
693
  return replay(tx, prior);
667
- const access = await assertReviewableResourceAccess(args.resourceType, args.resourceId, { ...ctx, transaction: tx }, "commenter");
694
+ const access = await assertReviewableResourceAccess(args.resourceType, args.resourceId, { ...ctx, transaction: tx }, reviewSuggestionAuthorRole(args.resourceType));
668
695
  const proposal = args.proposalId
669
696
  ? await getSuggestionProposal(tx, args.proposalId)
670
697
  : await insertSuggestionProposal(tx, {
@@ -675,11 +702,35 @@ export const createResourceSuggestionProposal = defineAction({
675
702
  authorEmail,
676
703
  actorKind,
677
704
  });
678
- if (!proposal)
705
+ if (!proposal) {
706
+ // A first call returns both a suggestion ID and a proposal ID, and
707
+ // agents appending a second edit often pass the suggestion's. Name
708
+ // the proposal when that suggestion is the caller's own, on this
709
+ // same resource: only its author can append to it.
710
+ const suggestion = args.proposalId
711
+ ? await getSuggestion(args.proposalId, tx)
712
+ : null;
713
+ if (suggestion &&
714
+ suggestion.resourceType === args.resourceType &&
715
+ suggestion.resourceId === args.resourceId &&
716
+ suggestion.authorEmail === authorEmail &&
717
+ suggestion.actorKind === actorKind) {
718
+ fail(suggestion.proposalId
719
+ ? `${suggestion.id} is a suggestion ID, not a proposal ID. To add this edit to the same review, pass its proposal ID: ${suggestion.proposalId}.`
720
+ : `${suggestion.id} is a suggestion ID, not a proposal ID, and it belongs to no proposal. Leave the proposal ID out to start a new review.`, {
721
+ statusCode: 400,
722
+ errorCode: "suggestion_id_not_proposal_id",
723
+ details: {
724
+ suggestionId: suggestion.id,
725
+ proposalId: suggestion.proposalId ?? null,
726
+ },
727
+ });
728
+ }
679
729
  fail("Proposal not found", {
680
730
  statusCode: 404,
681
731
  errorCode: "not_found",
682
732
  });
733
+ }
683
734
  if (proposal.resourceType !== args.resourceType ||
684
735
  proposal.resourceId !== args.resourceId ||
685
736
  proposal.adapterKind !== args.adapterKind ||
@@ -716,6 +767,8 @@ export const createResourceSuggestionProposal = defineAction({
716
767
  threadId: `suggestion-thread-${globalThis.crypto.randomUUID()}`,
717
768
  authorEmail,
718
769
  actorKind,
770
+ authorSource: suggestionAuthorSource(ctx),
771
+ authorClient: suggestionAuthorClient(ctx),
719
772
  baseRevision: args.baseRevision,
720
773
  status: "pending",
721
774
  summary: item.summary,
@@ -733,6 +786,7 @@ export const createResourceSuggestionProposal = defineAction({
733
786
  targetId: created.id,
734
787
  kind: "correction",
735
788
  anchor: created.operations.map((part) => part.anchor ?? null),
789
+ anchorKind: REVIEW_SUGGESTION_ANCHOR_KIND,
736
790
  body: created.summary,
737
791
  authorEmail,
738
792
  createdBy: actorKind,
@@ -998,4 +1052,5 @@ export const decideResourceSuggestionProposal = defineAction({
998
1052
  : undefined;
999
1053
  },
1000
1054
  },
1055
+ changeResource: (_input, result) => decisionChangeResource(result),
1001
1056
  });
@@ -1,3 +1,8 @@
1
+ import type { SuggestionAuthorSource } from "./types.js";
1
2
  export declare function suggestionActorKind(ctx: unknown): "agent" | "human" | "system";
3
+ /** How an agent's call arrived, or null for a person's. */
4
+ export declare function suggestionAuthorSource(ctx: unknown): SuggestionAuthorSource | null;
5
+ /** The MCP host an MCP call's client said it was, such as `claude-code`. */
6
+ export declare function suggestionAuthorClient(ctx: unknown): string | null;
2
7
  export declare const LEGACY_SUGGESTION_RECEIPT_VERSION = 1;
3
8
  export declare function suggestionActorKindMatchesReceipt(receiptActorKind: string | null | undefined, actorKind: string, receiptVersion: number | null | undefined): boolean;
@@ -1,3 +1,4 @@
1
+ import { resolveMcpClientHostId } from "../../shared/mcp-connect-content.js";
1
2
  export function suggestionActorKind(ctx) {
2
3
  const agentCallers = new Set(["agent", "tool", "mcp", "webmcp", "a2a"]);
3
4
  const caller = ctx?.caller;
@@ -5,6 +6,25 @@ export function suggestionActorKind(ctx) {
5
6
  return "agent";
6
7
  return ctx?.userEmail ? "human" : "system";
7
8
  }
9
+ /** How an agent's call arrived, or null for a person's. */
10
+ export function suggestionAuthorSource(ctx) {
11
+ const caller = ctx?.caller;
12
+ if (caller === "mcp" || caller === "webmcp")
13
+ return "mcp";
14
+ if (caller === "a2a")
15
+ return "a2a";
16
+ if (caller === "agent" || caller === "tool")
17
+ return "agent";
18
+ return null;
19
+ }
20
+ /** The MCP host an MCP call's client said it was, such as `claude-code`. */
21
+ export function suggestionAuthorClient(ctx) {
22
+ if (suggestionAuthorSource(ctx) !== "mcp")
23
+ return null;
24
+ // An MCP caller means ctx is a context object.
25
+ const client = ctx.mcpClient;
26
+ return resolveMcpClientHostId(client) ?? null;
27
+ }
8
28
  export const LEGACY_SUGGESTION_RECEIPT_VERSION = 1;
9
29
  export function suggestionActorKindMatchesReceipt(receiptActorKind, actorKind, receiptVersion) {
10
30
  if (receiptActorKind === actorKind)
@@ -69,6 +69,14 @@ export const reviewSuggestionsStore = defineStore({
69
69
  await exec.execute("CREATE INDEX IF NOT EXISTS idx_review_suggestion_proposal_members ON agent_review_suggestions (proposal_id, created_at)");
70
70
  },
71
71
  },
72
+ {
73
+ name: "author-source-and-client",
74
+ run: async () => {
75
+ for (const column of ["author_source", "author_client"]) {
76
+ await ensureColumnExists("agent_review_suggestions", column, `ALTER TABLE agent_review_suggestions ADD COLUMN IF NOT EXISTS ${column} TEXT`);
77
+ }
78
+ },
79
+ },
72
80
  ],
73
81
  });
74
82
  export function ensureSuggestionTables(client = getDbExec()) {
@@ -143,7 +151,7 @@ export async function insertSuggestion(input, client = getDbExec()) {
143
151
  const suggestionId = `suggestion-${newId()}`;
144
152
  const now = new Date().toISOString();
145
153
  await client.execute({
146
- sql: "INSERT INTO agent_review_suggestions (id,resource_type,resource_id,adapter_kind,adapter_version,thread_id,author_email,actor_kind,base_revision,status,summary,owner_email,org_id,visibility,created_at,updated_at,metadata_json,proposal_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
154
+ sql: "INSERT INTO agent_review_suggestions (id,resource_type,resource_id,adapter_kind,adapter_version,thread_id,author_email,actor_kind,author_source,author_client,base_revision,status,summary,owner_email,org_id,visibility,created_at,updated_at,metadata_json,proposal_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
147
155
  args: [
148
156
  suggestionId,
149
157
  input.resourceType,
@@ -153,6 +161,8 @@ export async function insertSuggestion(input, client = getDbExec()) {
153
161
  input.threadId,
154
162
  input.authorEmail,
155
163
  input.actorKind,
164
+ input.authorSource ?? null,
165
+ input.authorClient ?? null,
156
166
  input.baseRevision,
157
167
  input.status,
158
168
  input.summary,
@@ -307,6 +317,9 @@ function suggestionFromRows(row, operationRows) {
307
317
  threadId: String(row.thread_id),
308
318
  authorEmail: row.author_email,
309
319
  actorKind: row.actor_kind,
320
+ authorSource: (row.author_source ??
321
+ null),
322
+ authorClient: (row.author_client ?? null),
310
323
  baseRevision: String(row.base_revision),
311
324
  status: row.status,
312
325
  summary: String(row.summary),
@@ -336,8 +349,9 @@ export async function listSuggestions(resourceType, resourceId, statuses, client
336
349
  ? ` AND s.status IN (${statuses.map(() => "?").join(",")})`
337
350
  : "";
338
351
  args.push(...(statuses ?? []));
352
+ // raw-sql-ok: the filter adds only ? placeholders, one per bound status.
339
353
  const rows = (await client.execute({
340
- sql: `SELECT s.id AS suggestion_id,s.proposal_id,p.summary AS proposal_summary,s.revision,s.resource_type,s.resource_id,s.adapter_kind,s.adapter_version,s.thread_id,s.author_email,s.actor_kind,s.base_revision,s.status,s.summary,s.owner_email,s.org_id,s.visibility,s.created_at,s.updated_at,s.metadata_json,o.id AS operation_id,o.ordinal AS operation_ordinal,o.operation_kind,o.target_id,o.before_json,o.after_json,o.anchor_json,o.dependencies_json,o.schema_version FROM agent_review_suggestions s LEFT JOIN agent_review_suggestion_proposals p ON p.id = s.proposal_id LEFT JOIN agent_review_suggestion_operations o ON o.suggestion_id = s.id WHERE s.resource_type = ? AND s.resource_id = ?${filter} ORDER BY s.created_at,o.ordinal`,
354
+ sql: `SELECT s.id AS suggestion_id,s.proposal_id,p.summary AS proposal_summary,s.revision,s.resource_type,s.resource_id,s.adapter_kind,s.adapter_version,s.thread_id,s.author_email,s.actor_kind,s.author_source,s.author_client,s.base_revision,s.status,s.summary,s.owner_email,s.org_id,s.visibility,s.created_at,s.updated_at,s.metadata_json,o.id AS operation_id,o.ordinal AS operation_ordinal,o.operation_kind,o.target_id,o.before_json,o.after_json,o.anchor_json,o.dependencies_json,o.schema_version FROM agent_review_suggestions s LEFT JOIN agent_review_suggestion_proposals p ON p.id = s.proposal_id LEFT JOIN agent_review_suggestion_operations o ON o.suggestion_id = s.id WHERE s.resource_type = ? AND s.resource_id = ?${filter} ORDER BY s.created_at,o.ordinal`,
341
355
  args,
342
356
  })).rows;
343
357
  const grouped = new Map();
@@ -1,6 +1,7 @@
1
1
  import type { Visibility } from "../../sharing/schema.js";
2
2
  export type SuggestionStatus = "pending" | "accepted" | "rejected" | "stale" | "superseded" | "withdrawn";
3
3
  export type SuggestionDecision = "accepted" | "rejected";
4
+ export type SuggestionAuthorSource = "mcp" | "a2a" | "agent";
4
5
  export interface SuggestionOperation {
5
6
  id?: string;
6
7
  ordinal: number;
@@ -24,6 +25,18 @@ export interface ResourceSuggestion {
24
25
  threadId: string;
25
26
  authorEmail: string | null;
26
27
  actorKind: "human" | "agent" | "system";
28
+ /**
29
+ * How an agent's suggestion arrived: over MCP, from another app's agent
30
+ * (`a2a`), or from the app's own agent. Null for people and for
31
+ * suggestions made before it was recorded.
32
+ */
33
+ authorSource?: SuggestionAuthorSource | null;
34
+ /**
35
+ * The MCP host the agent's client said it was, as a connect host id such as
36
+ * `claude-code` (see `mcpConnectHostLabel`). Null when the client named no
37
+ * host this app knows.
38
+ */
39
+ authorClient?: string | null;
27
40
  baseRevision: string;
28
41
  status: SuggestionStatus;
29
42
  summary: string;
@@ -1,3 +1,4 @@
1
+ import type { DbExec } from "../db/client.js";
1
2
  import type { Visibility } from "../sharing/schema.js";
2
3
  export type ReviewResourceRole = "viewer" | "commenter" | "editor" | "admin" | "owner";
3
4
  export type ReviewCommentKind = "comment" | "annotation" | "correction" | "question" | "decision" | "review";
@@ -24,6 +25,77 @@ export interface ReviewableResourceRegistration {
24
25
  displayName?: string;
25
26
  resolveUrl?: (resourceId: string) => Promise<string | null | undefined> | string | null | undefined;
26
27
  resolveAccess?: (resourceId: string, ctx?: ReviewResourceContext) => Promise<ReviewResourceAccess | null> | ReviewResourceAccess | null;
28
+ /** Who may comment, and where a new thread goes. */
29
+ comments?: ReviewCommentPolicy;
30
+ /**
31
+ * What a comment can attach to, by anchor kind. Once a resource declares
32
+ * any, every anchored comment on it names one, and its anchor must pass that
33
+ * kind's check before anything is written.
34
+ */
35
+ anchorKinds?: Record<string, ReviewAnchorKind>;
36
+ /**
37
+ * Runs inside the same database transaction as each comment write, with
38
+ * that transaction's handle. Throwing refuses the write. Use the handle for
39
+ * any database work here: `getDbExec()` would run on another connection.
40
+ */
41
+ onCommentWrite?: (event: ReviewCommentWriteEvent, tx: DbExec) => Promise<void> | void;
42
+ }
43
+ export interface ReviewCommentPolicy {
44
+ /**
45
+ * The lowest role that may comment, reply and react, and edit or delete
46
+ * their own comments. Defaults to `commenter`.
47
+ */
48
+ minimumRole?: ReviewResourceRole;
49
+ /**
50
+ * Where a new thread that mentions no one goes: the agent (the default) or
51
+ * people. A thread with a mention always goes to people.
52
+ */
53
+ unmentionedRoute?: ReviewResolutionTarget;
54
+ }
55
+ export interface ReviewAnchorKind {
56
+ /** The version new anchors of this kind are written at. */
57
+ version: number;
58
+ /**
59
+ * Checks an anchor written at `version` and returns it as it's stored.
60
+ * Throw to refuse a malformed anchor; nothing is written.
61
+ */
62
+ parse: (anchor: unknown, version: number) => unknown;
63
+ }
64
+ /** A comment write, as a resource's `onCommentWrite` hook sees it. */
65
+ export type ReviewCommentWriteEvent = {
66
+ type: "created";
67
+ comment: ReviewComment;
68
+ } | {
69
+ type: "edited" | "anchor-moved";
70
+ comment: ReviewComment;
71
+ /** Who changed it, which for an editor moving a pin isn't the author. */
72
+ actorEmail: string | null;
73
+ } | {
74
+ type: "resolved" | "reopened";
75
+ resourceType: string;
76
+ resourceId: string;
77
+ threadId: string;
78
+ actorEmail: string | null;
79
+ } | {
80
+ type: "deleted";
81
+ resourceType: string;
82
+ resourceId: string;
83
+ threadId: string;
84
+ /** The comment deleted and, for a thread's first comment, the rest. */
85
+ commentIds: string[];
86
+ actorEmail: string | null;
87
+ };
88
+ /** A comment's link to the same comment or thread in another system. */
89
+ export interface ReviewCommentLink {
90
+ /** The other system, such as `notion`. */
91
+ provider: string;
92
+ /** What the outside ID names there, such as `comment` or `discussion`. */
93
+ externalKind: string;
94
+ externalId: string;
95
+ commentId: string;
96
+ resourceType: string;
97
+ resourceId: string;
98
+ createdAt: string;
27
99
  }
28
100
  export interface ReviewMention {
29
101
  label: string;
@@ -52,9 +124,26 @@ export interface ReviewComment {
52
124
  threadId: string;
53
125
  parentCommentId: string | null;
54
126
  targetId: string | null;
127
+ /**
128
+ * The part of the resource the thread sits on, such as a slide. The kit
129
+ * always returns this and the anchor and revision fields below; they're
130
+ * optional so comments built elsewhere, such as optimistic ones, needn't.
131
+ */
132
+ surfaceId?: string | null;
55
133
  kind: ReviewCommentKind;
56
134
  status: ReviewCommentStatus;
57
135
  anchor: unknown;
136
+ /** The anchor's kind and the version it was written at, when typed. */
137
+ anchorKind?: string | null;
138
+ anchorVersion?: number | null;
139
+ /**
140
+ * Goes up whenever this comment changes: its text, anchor, status or
141
+ * deletion, and on a thread's first comment, which holds the thread's
142
+ * routing, its routing and agent pickup. So a write can check nothing
143
+ * changed under it. Notification bookkeeping leaves it alone, so a new
144
+ * comment's author can edit it at the revision they got back.
145
+ */
146
+ revision?: number;
58
147
  body: string;
59
148
  authorEmail: string | null;
60
149
  authorName: string | null;
@@ -5,6 +5,8 @@ export interface TryForwardDbQueryOptions {
5
5
  format?: string;
6
6
  userEmail?: string;
7
7
  orgId?: string;
8
+ /** The caller acts in no org; the dev server must not look one up. */
9
+ orgScope?: "personal";
8
10
  print: (rows: Record<string, unknown>[], sql: string, format?: string) => void;
9
11
  }
10
12
  export declare function tryForwardDbQueryToDevServer(options: TryForwardDbQueryOptions): Promise<boolean>;
@@ -1,5 +1,5 @@
1
1
  import { Agent } from "undici";
2
- import { DEV_ACTION_ORG_HEADER, DEV_ACTION_TOKEN_HEADER, DEV_ACTION_USER_HEADER, DEV_DB_QUERY_ROUTE, } from "../../server/dev-action-bridge.js";
2
+ import { DEV_ACTION_ORG_HEADER, DEV_ACTION_ORG_SCOPE_HEADER, DEV_ACTION_TOKEN_HEADER, DEV_ACTION_USER_HEADER, DEV_DB_QUERY_ROUTE, } from "../../server/dev-action-bridge.js";
3
3
  import { discoverPgliteDevServer } from "./dev-server-discovery.js";
4
4
  export async function tryForwardDbQueryToDevServer(options) {
5
5
  const discovery = discoverPgliteDevServer();
@@ -23,6 +23,9 @@ export async function tryForwardDbQueryToDevServer(options) {
23
23
  ? { [DEV_ACTION_USER_HEADER]: options.userEmail }
24
24
  : {}),
25
25
  ...(options.orgId ? { [DEV_ACTION_ORG_HEADER]: options.orgId } : {}),
26
+ ...(options.orgScope === "personal"
27
+ ? { [DEV_ACTION_ORG_SCOPE_HEADER]: "personal" }
28
+ : {}),
26
29
  },
27
30
  body: JSON.stringify({
28
31
  sql: options.sql,
@@ -125,6 +125,9 @@ Options:
125
125
  format: parsed.format,
126
126
  userEmail: getRequestUserEmail(),
127
127
  orgId: getRequestOrgId() ?? undefined,
128
+ // The CLI already resolved this call's org; with none, it is personal,
129
+ // so the dev server must not look one up again.
130
+ ...(getRequestOrgId() ? {} : { orgScope: "personal" }),
128
131
  print: printTable,
129
132
  });
130
133
  if (forwarded)
@@ -1,6 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { getRequestOrgId, getRequestTimezone, getRequestUserEmail, getRequestUserName, hasRequestContext, } from "../../server/request-context.js";
4
+ import { AGENT_ORG_SCOPE_ENV } from "../dev-session.js";
4
5
  import { parseArgs } from "../utils.js";
5
6
  const MAX_OUTPUT = 50_000;
6
7
  const TIMEOUT_MS = 30_000;
@@ -66,6 +67,9 @@ function childProcessEnvForRequest() {
66
67
  setOrClearRequestEnv(env, "AGENT_USER_EMAIL", userEmail, requestScoped);
67
68
  setOrClearRequestEnv(env, "AGENT_USER_NAME", userName, requestScoped);
68
69
  setOrClearRequestEnv(env, "AGENT_ORG_ID", orgId, requestScoped);
70
+ // A request with no org runs its commands with no org too. Without this, a
71
+ // `pnpm action` it runs would look up the user's active org instead.
72
+ setOrClearRequestEnv(env, AGENT_ORG_SCOPE_ENV, requestScoped && !orgId ? "personal" : undefined, requestScoped);
69
73
  setOrClearRequestEnv(env, "AGENT_USER_TIMEZONE", timezone, requestScoped);
70
74
  return env;
71
75
  }
@@ -7,3 +7,34 @@
7
7
  * (sign in via the running app, or set `AGENT_USER_EMAIL`).
8
8
  */
9
9
  export declare function resolveDevUserEmail(): Promise<string | undefined>;
10
+ /**
11
+ * Set to "personal" on a child process whose caller acts in no org, so a
12
+ * `pnpm action` it runs stays out of the user's orgs too. The agent's shell
13
+ * tool sets it for a request with no org.
14
+ */
15
+ export declare const AGENT_ORG_SCOPE_ENV = "AGENT_ORG_SCOPE";
16
+ export interface DevOrgChoice {
17
+ /** An org the caller named: `AGENT_ORG_ID` or a forwarded org header. */
18
+ orgId?: string | undefined;
19
+ /** The caller acts in no org and must not be given one. */
20
+ personal?: boolean | undefined;
21
+ }
22
+ /** The org choice this CLI process inherited from its environment. */
23
+ export declare function devOrgChoiceFromEnv(env?: NodeJS.ProcessEnv): DevOrgChoice;
24
+ /**
25
+ * Resolve the org scope a local CLI call acts in, for `userEmail`, as fields
26
+ * for its request context.
27
+ *
28
+ * An inherited personal scope wins, then a named org. Otherwise this is the
29
+ * org the signed-in app uses for the same user: their active org, else their
30
+ * first membership, or none when they chose their personal space or have no
31
+ * org. Without it, `pnpm action` ran with no org and saw only the user's
32
+ * personal rows while the app showed their org's.
33
+ *
34
+ * Like the app, a membership table that can't be read counts as no org;
35
+ * transient database errors still throw.
36
+ */
37
+ export declare function resolveDevOrgScope(userEmail: string | undefined, choice: DevOrgChoice): Promise<{
38
+ orgId?: string;
39
+ orgScope?: "personal";
40
+ }>;
@@ -52,3 +52,61 @@ export async function resolveDevUserEmail() {
52
52
  return undefined;
53
53
  }
54
54
  }
55
+ /**
56
+ * Set to "personal" on a child process whose caller acts in no org, so a
57
+ * `pnpm action` it runs stays out of the user's orgs too. The agent's shell
58
+ * tool sets it for a request with no org.
59
+ */
60
+ export const AGENT_ORG_SCOPE_ENV = "AGENT_ORG_SCOPE";
61
+ /** The org choice this CLI process inherited from its environment. */
62
+ export function devOrgChoiceFromEnv(env = process.env) {
63
+ return {
64
+ orgId: env.AGENT_ORG_ID || undefined,
65
+ personal: env[AGENT_ORG_SCOPE_ENV] === "personal",
66
+ };
67
+ }
68
+ /**
69
+ * Resolve the org scope a local CLI call acts in, for `userEmail`, as fields
70
+ * for its request context.
71
+ *
72
+ * An inherited personal scope wins, then a named org. Otherwise this is the
73
+ * org the signed-in app uses for the same user: their active org, else their
74
+ * first membership, or none when they chose their personal space or have no
75
+ * org. Without it, `pnpm action` ran with no org and saw only the user's
76
+ * personal rows while the app showed their org's.
77
+ *
78
+ * Like the app, a membership table that can't be read counts as no org;
79
+ * transient database errors still throw.
80
+ */
81
+ export async function resolveDevOrgScope(userEmail, choice) {
82
+ // Personal comes first. Only the agent's shell sets it, for a request with
83
+ // no org, and the child's own env loading can put an AGENT_ORG_ID from
84
+ // `.env` back, which must not give the command an org.
85
+ if (choice.personal)
86
+ return { orgScope: "personal" };
87
+ if (choice.orgId)
88
+ return { orgId: choice.orgId };
89
+ if (!userEmail)
90
+ return {};
91
+ if (await localDatabaseIsMissing())
92
+ return {};
93
+ const { resolveOrgIdForEmail } = await import("../org/context.js");
94
+ const orgId = await resolveOrgIdForEmail(userEmail);
95
+ return orgId ? { orgId } : {};
96
+ }
97
+ /**
98
+ * True when the app's local PGlite database hasn't been created yet. It has
99
+ * no orgs, and opening it to look would create it and run every migration,
100
+ * which takes many seconds for an action that may not touch the database.
101
+ */
102
+ async function localDatabaseIsMissing() {
103
+ const { getRuntimeDatabaseUrl, isPgliteUrl, pgliteDataDirFromUrl, pgliteRuntimeDataDir, } = await import("../db/client.js");
104
+ const url = getRuntimeDatabaseUrl("pglite:./data/pglite");
105
+ if (!isPgliteUrl(url))
106
+ return false;
107
+ const dataDir = pgliteRuntimeDataDir(pgliteDataDirFromUrl(url));
108
+ if (dataDir === "memory://")
109
+ return false;
110
+ const fs = await import("node:fs");
111
+ return !fs.existsSync(dataDir);
112
+ }