@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049

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 (91) hide show
  1. package/README.md +1 -1
  2. package/dist/app-config/analytics.d.ts +1 -0
  3. package/dist/app-config/analytics.js +7 -0
  4. package/dist/app-config/schema.d.ts +1 -0
  5. package/dist/chat-threads/store.d.ts +28 -2
  6. package/dist/chat-threads/store.js +75 -13
  7. package/dist/cli/template-baseline.js +18 -5
  8. package/dist/cli/template-sync.js +100 -19
  9. package/dist/client/agent-chat/index.d.ts +2 -1
  10. package/dist/client/agent-chat/index.js +2 -1
  11. package/dist/client/chat/agentkit-protocol.js +21 -1
  12. package/dist/client/chat/run-outcome.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +6 -0
  14. package/dist/client/sharing/index.d.ts +2 -1
  15. package/dist/client/sharing/index.js +2 -1
  16. package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
  17. package/dist/client/sharing/share-controller-helpers.js +46 -1
  18. package/dist/client/sharing/useShareButtonController.d.ts +12 -0
  19. package/dist/client/sharing/useShareButtonController.js +76 -57
  20. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  21. package/dist/client/sharing/useShareDialogController.js +77 -49
  22. package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
  23. package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
  24. package/dist/client/use-chat-thread-list.d.ts +40 -0
  25. package/dist/client/use-chat-thread-list.js +210 -0
  26. package/dist/client/use-chat-threads.d.ts +67 -0
  27. package/dist/client/use-chat-threads.js +141 -23
  28. package/dist/client/use-db-sync.d.ts +1 -0
  29. package/dist/client/use-db-sync.js +13 -1
  30. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  31. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  34. package/dist/collab/bounded-task-bundle.js +39 -0
  35. package/dist/collab/bounded-task-source.d.ts +14 -0
  36. package/dist/collab/bounded-task-source.js +8 -0
  37. package/dist/collab/bounded-tasks.d.ts +67 -0
  38. package/dist/collab/bounded-tasks.js +439 -0
  39. package/dist/collab/client.js +2 -1
  40. package/dist/collab/index.d.ts +5 -2
  41. package/dist/collab/index.js +5 -2
  42. package/dist/collab/live-copy-tasks.d.ts +80 -0
  43. package/dist/collab/live-copy-tasks.js +330 -0
  44. package/dist/collab/live-copy.d.ts +188 -0
  45. package/dist/collab/live-copy.js +415 -0
  46. package/dist/collab/routes.d.ts +5 -0
  47. package/dist/collab/routes.js +24 -8
  48. package/dist/collab/storage.d.ts +53 -1
  49. package/dist/collab/storage.js +93 -5
  50. package/dist/collab/struct-routes.d.ts +1 -1
  51. package/dist/collab/ydoc-manager.d.ts +92 -3
  52. package/dist/collab/ydoc-manager.js +447 -38
  53. package/dist/db/client.d.ts +6 -0
  54. package/dist/db/client.js +25 -5
  55. package/dist/deploy/build.js +2 -0
  56. package/dist/extensions/actions.js +1 -0
  57. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  58. package/dist/mcp/agent-connected.js +5 -1
  59. package/dist/mcp/build-server.js +39 -10
  60. package/dist/notifications/routes.d.ts +1 -1
  61. package/dist/observability/routes.d.ts +1 -1
  62. package/dist/org/index.d.ts +2 -2
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  65. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  66. package/dist/server/agent-chat-plugin.js +80 -17
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  69. package/dist/server/analytics.d.ts +1 -0
  70. package/dist/server/analytics.js +18 -1
  71. package/dist/server/collab-plugin.d.ts +10 -1
  72. package/dist/server/collab-plugin.js +89 -20
  73. package/dist/server/index.d.ts +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/shared/optional-node-builtins.d.ts +1 -0
  76. package/dist/shared/optional-node-builtins.js +3 -0
  77. package/dist/sharing/access.d.ts +1 -0
  78. package/dist/sharing/access.js +113 -95
  79. package/dist/tracking/providers.d.ts +1 -0
  80. package/dist/tracking/providers.js +2 -1
  81. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  82. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  83. package/dist/vite/bounded-task-plugin.js +44 -0
  84. package/dist/vite/client.js +2 -0
  85. package/dist/vitest-config.js +3 -0
  86. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  87. package/docs/content/deployment-environment-variables.mdx +1 -0
  88. package/docs/content/environment-variables.mdx +1 -0
  89. package/docs/content/sharing.mdx +36 -0
  90. package/docs/content/toolkit-sharing.mdx +98 -12
  91. package/package.json +5 -3
package/README.md CHANGED
@@ -280,7 +280,7 @@ Explore the [full app gallery](https://agent-native.com/apps?utm_source=npm&utm_
280
280
 
281
281
  ## Contributing
282
282
 
283
- Working on this repository itself? See [DEVELOPMENT.md](./DEVELOPMENT.md) for local setup, workspace structure, and guard scripts.
283
+ Want to send a change back? Start with [CONTRIBUTING.md](./CONTRIBUTING.md). [DEVELOPMENT.md](./DEVELOPMENT.md) covers local setup, workspace structure, and guard scripts.
284
284
 
285
285
  ## License
286
286
 
@@ -3,4 +3,5 @@ export declare const analyticsConfig: z.ZodObject<{
3
3
  authSessionReplay: z.ZodDefault<z.ZodBoolean>;
4
4
  agentNativePublicKey: z.ZodOptional<z.ZodString>;
5
5
  agentNativeEndpoint: z.ZodDefault<z.ZodString>;
6
+ headScriptSrc: z.ZodOptional<z.ZodURL>;
6
7
  }, z.core.$strip>;
@@ -27,4 +27,11 @@ export const analyticsConfig = z.object({
27
27
  ],
28
28
  doc: "Endpoint for first-party Agent-Native Analytics events.",
29
29
  }),
30
+ headScriptSrc: z
31
+ .url({ protocol: /^https$/ })
32
+ .optional()
33
+ .meta({
34
+ env: "AGENT_NATIVE_HEAD_SCRIPT_SRC",
35
+ doc: "HTTPS URL of a script loaded with `defer` on every server-rendered page, for deployment-specific tooling such as support or feedback widgets.",
36
+ }),
30
37
  });
@@ -44,6 +44,7 @@ export declare const appConfigSchema: z.ZodObject<{
44
44
  authSessionReplay: z.ZodDefault<z.ZodBoolean>;
45
45
  agentNativePublicKey: z.ZodOptional<z.ZodString>;
46
46
  agentNativeEndpoint: z.ZodDefault<z.ZodString>;
47
+ headScriptSrc: z.ZodOptional<z.ZodURL>;
47
48
  }, z.core.$strip>>;
48
49
  app: z.ZodPrefault<z.ZodObject<{
49
50
  id: z.ZodOptional<z.ZodString>;
@@ -58,8 +58,24 @@ export interface ChatThread {
58
58
  orgId: string | null;
59
59
  visibility: "private" | "org" | "public";
60
60
  }
61
+ /**
62
+ * Which of the caller's lists a thread belongs to. `mine` is ownership alone,
63
+ * so a chat the caller owns stays in it however widely it is shared; `shared`
64
+ * is accessible-but-owned-by-someone-else.
65
+ */
66
+ export type ChatThreadOwnership = "all" | "mine" | "shared";
67
+ export declare const CHAT_THREAD_OWNERSHIPS: readonly ChatThreadOwnership[];
68
+ export declare function isChatThreadOwnership(value: unknown): value is ChatThreadOwnership;
69
+ /**
70
+ * Why the caller can see a thread. A direct per-user grant outranks
71
+ * organization access when both apply, so revoking the organization grant never
72
+ * looks like it removed access the person was given by name.
73
+ */
74
+ export type ChatThreadAccessBasis = "owner" | "direct" | "organization";
61
75
  export interface ChatThreadSummary {
62
76
  id: string;
77
+ ownerEmail?: string;
78
+ accessBasis?: ChatThreadAccessBasis;
63
79
  title: string;
64
80
  preview: string;
65
81
  messageCount: number;
@@ -72,6 +88,11 @@ export interface ChatThreadSummary {
72
88
  orgId: string | null;
73
89
  visibility: "private" | "org" | "public";
74
90
  }
91
+ /** List and search rows always include the caller's ownership and access basis. */
92
+ export interface ChatThreadListRow extends ChatThreadSummary {
93
+ ownerEmail: string;
94
+ accessBasis: ChatThreadAccessBasis;
95
+ }
75
96
  export interface ForkThreadSourceSnapshot {
76
97
  threadData: string;
77
98
  title?: string;
@@ -148,8 +169,10 @@ export interface ListThreadsOptions {
148
169
  includeExternal?: boolean;
149
170
  /** Current app id used by the local-only view. */
150
171
  sourceAppId?: string | null;
172
+ /** Defaults to `all`. Narrowed in SQL, so LIMIT/OFFSET page within the list. */
173
+ ownership?: ChatThreadOwnership;
151
174
  }
152
- export declare function listThreads(ownerEmail: string, options?: ListThreadsOptions | number, legacyOffset?: number): Promise<ChatThreadSummary[]>;
175
+ export declare function listThreads(ownerEmail: string, options?: ListThreadsOptions | number, legacyOffset?: number): Promise<ChatThreadListRow[]>;
153
176
  export declare function searchThreads(ownerEmail: string, query: string, limit?: number, options?: {
154
177
  scope?: {
155
178
  type: string;
@@ -162,7 +185,10 @@ export declare function searchThreads(ownerEmail: string, query: string, limit?:
162
185
  includeExternal?: boolean;
163
186
  /** Current app id used by the local-only view. */
164
187
  sourceAppId?: string | null;
165
- }): Promise<ChatThreadSummary[]>;
188
+ /** See `ListThreadsOptions.ownership`. */
189
+ ownership?: ChatThreadOwnership;
190
+ offset?: number;
191
+ }): Promise<ChatThreadListRow[]>;
166
192
  /**
167
193
  * Scope a thread should carry after a run inside a resource: adopt when it has
168
194
  * none, otherwise keep what it has. An unscoped thread reads as general, and a
@@ -12,7 +12,7 @@ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
12
12
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
13
13
  import { getRequestOrgId } from "../server/request-context.js";
14
14
  import { assertNoInlineImageBytes, stripInlineBytes, } from "../shared/inline-bytes.js";
15
- import { resolveAccess } from "../sharing/access.js";
15
+ import { hasCurrentOrgMembership, resolveAccess, } from "../sharing/access.js";
16
16
  import { registerShareableResource } from "../sharing/registry.js";
17
17
  import { roleSatisfies } from "../sharing/schema.js";
18
18
  import { emitChatThreadChange } from "./emitter.js";
@@ -226,6 +226,14 @@ export function threadScopeMismatch(existing, incoming) {
226
226
  return isAppOwnedChatScope(existing);
227
227
  return existing.type !== incoming.type || existing.id !== incoming.id;
228
228
  }
229
+ export const CHAT_THREAD_OWNERSHIPS = [
230
+ "all",
231
+ "mine",
232
+ "shared",
233
+ ];
234
+ export function isChatThreadOwnership(value) {
235
+ return CHAT_THREAD_OWNERSHIPS.includes(value);
236
+ }
229
237
  function readScope(r) {
230
238
  const type = r.scope_type;
231
239
  const id = r.scope_id;
@@ -629,6 +637,12 @@ function rowToThread(r) {
629
637
  visibility: readVisibility(r.visibility),
630
638
  };
631
639
  }
640
+ function readAccessBasis(value) {
641
+ if (value === "owner" || value === "direct" || value === "organization") {
642
+ return value;
643
+ }
644
+ throw new Error(`Unexpected chat thread access basis: ${String(value)}`);
645
+ }
632
646
  function rowToSummary(r) {
633
647
  // The summary path never loads `thread_data`; the count comes from the
634
648
  // dedicated `message_count` column maintained on write. Empty threads are
@@ -638,6 +652,8 @@ function rowToSummary(r) {
638
652
  return null;
639
653
  return {
640
654
  id: r.id,
655
+ ownerEmail: r.owner_email,
656
+ accessBasis: readAccessBasis(r.access_basis),
641
657
  title: r.title,
642
658
  preview: r.preview,
643
659
  messageCount,
@@ -712,7 +728,7 @@ const THREAD_COLUMNS = `id, owner_email, title, preview, thread_data, message_co
712
728
  // from the blob anymore — preview and message_count are dedicated columns
713
729
  // (message_count is maintained on write). The detail path (`THREAD_COLUMNS` /
714
730
  // `getThread`) still returns the full blob.
715
- const SUMMARY_COLUMNS = `id, title, preview, message_count, created_at, updated_at, scope_type, scope_id, scope_label, pinned_at, archived_at, source_platform, source_app_id, source_url, org_id, visibility`;
731
+ const SUMMARY_COLUMNS = `id, owner_email, title, preview, message_count, created_at, updated_at, scope_type, scope_id, scope_label, pinned_at, archived_at, source_platform, source_app_id, source_url, org_id, visibility`;
716
732
  export function registerChatThreadsShareable() {
717
733
  registerShareableResource({
718
734
  type: "chat_thread",
@@ -756,7 +772,7 @@ export async function resolveThreadsAccess(userEmail, threadIds, ctx = {}) {
756
772
  if (!userEmail || ids.length === 0)
757
773
  return threads;
758
774
  await ensureTable();
759
- const access = chatThreadAccessSql(userEmail, ctx.orgId);
775
+ const access = await chatThreadAccessSql(userEmail, ctx.orgId);
760
776
  const client = getDbExec();
761
777
  const placeholders = ids.map(() => "?").join(", ");
762
778
  const { rows } = await client.execute({
@@ -896,14 +912,15 @@ export async function forkThread(sourceId, ownerEmail, opts) {
896
912
  visibility: "private",
897
913
  };
898
914
  }
899
- function chatThreadAccessSql(userEmail, orgId) {
900
- const normalizedEmail = userEmail.trim().toLowerCase();
915
+ async function chatThreadAccessSql(userEmail, orgId) {
916
+ const normalizedEmail = normalizeAccessEmail(userEmail);
901
917
  const clauses = [
902
918
  `LOWER(owner_email) = ?`,
903
919
  `EXISTS (SELECT 1 FROM chat_thread_shares WHERE chat_thread_shares.resource_id = chat_threads.id AND chat_thread_shares.principal_type = 'user' AND LOWER(chat_thread_shares.principal_id) = ?)`,
904
920
  ];
905
921
  const args = [normalizedEmail, normalizedEmail];
906
- if (orgId) {
922
+ if (orgId &&
923
+ (await hasCurrentOrgMembership({ getDb: getChatThreadsDb }, orgId, normalizedEmail, { userEmail: normalizedEmail, orgId }))) {
907
924
  clauses.push(`(visibility = 'org' AND org_id = ?)`);
908
925
  args.push(orgId);
909
926
  clauses.push(`EXISTS (SELECT 1 FROM chat_thread_shares WHERE chat_thread_shares.resource_id = chat_threads.id AND chat_thread_shares.principal_type = 'org' AND chat_thread_shares.principal_id = ?)`);
@@ -911,6 +928,33 @@ function chatThreadAccessSql(userEmail, orgId) {
911
928
  }
912
929
  return { sql: `(${clauses.join(" OR ")})`, args };
913
930
  }
931
+ function normalizeAccessEmail(userEmail) {
932
+ return userEmail.trim().toLowerCase();
933
+ }
934
+ /**
935
+ * Summary projection plus the caller's access basis, derived from the same
936
+ * owner and per-user-share comparisons the ACL uses. Anything that is neither
937
+ * owned nor directly granted reached the caller through the organization.
938
+ */
939
+ function chatThreadSummarySelect(userEmail) {
940
+ const normalizedEmail = normalizeAccessEmail(userEmail);
941
+ return {
942
+ sql: `${SUMMARY_COLUMNS}, CASE WHEN LOWER(owner_email) = ? THEN 'owner' WHEN EXISTS (SELECT 1 FROM chat_thread_shares WHERE chat_thread_shares.resource_id = chat_threads.id AND chat_thread_shares.principal_type = 'user' AND LOWER(chat_thread_shares.principal_id) = ?) THEN 'direct' ELSE 'organization' END AS access_basis`,
943
+ args: [normalizedEmail, normalizedEmail],
944
+ };
945
+ }
946
+ function chatThreadOwnershipSql(userEmail, ownership) {
947
+ if (ownership === undefined || ownership === "all")
948
+ return null;
949
+ const normalizedEmail = normalizeAccessEmail(userEmail);
950
+ if (ownership === "mine") {
951
+ return { sql: `LOWER(owner_email) = ?`, args: [normalizedEmail] };
952
+ }
953
+ if (ownership === "shared") {
954
+ return { sql: `LOWER(owner_email) <> ?`, args: [normalizedEmail] };
955
+ }
956
+ throw new Error(`Unknown chat thread ownership: ${String(ownership)}`);
957
+ }
914
958
  export async function listThreads(ownerEmail, options = {}, legacyOffset) {
915
959
  await ensureTable();
916
960
  // Back-compat shim: previous signature was (owner, limit, offset).
@@ -926,9 +970,15 @@ export async function listThreads(ownerEmail, options = {}, legacyOffset) {
926
970
  // column, so nothing here may filter on `thread_data`. Matching that blob
927
971
  // detoasts the whole message history for every scanned row — before LIMIT
928
972
  // applies — which is what made this list cost seconds instead of milliseconds.
929
- const access = chatThreadAccessSql(ownerEmail, opts.orgId ?? getRequestOrgId());
973
+ const access = await chatThreadAccessSql(ownerEmail, opts.orgId === undefined ? getRequestOrgId() : opts.orgId);
974
+ const ownershipFilter = chatThreadOwnershipSql(ownerEmail, opts.ownership);
975
+ const select = chatThreadSummarySelect(ownerEmail);
930
976
  const filters = [access.sql, `message_count > 0`];
931
- const args = [...access.args];
977
+ const args = [...select.args, ...access.args];
978
+ if (ownershipFilter) {
979
+ filters.push(ownershipFilter.sql);
980
+ args.push(...ownershipFilter.args);
981
+ }
932
982
  if (!opts.includeArchived) {
933
983
  filters.push(`archived_at IS NULL`);
934
984
  }
@@ -948,7 +998,7 @@ export async function listThreads(ownerEmail, options = {}, legacyOffset) {
948
998
  }
949
999
  args.push(limit, offset);
950
1000
  const { rows } = await client.execute({
951
- sql: `SELECT ${SUMMARY_COLUMNS} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC LIMIT ? OFFSET ?`,
1001
+ sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
952
1002
  args,
953
1003
  });
954
1004
  return rows
@@ -965,13 +1015,25 @@ export async function searchThreads(ownerEmail, query, limit = 50, options = {})
965
1015
  // The count-guard uses the maintained `message_count` column (same as
966
1016
  // listThreads). The content match still scans `thread_data` — search
967
1017
  // legitimately needs to look inside message history.
968
- const access = chatThreadAccessSql(ownerEmail, options.orgId ?? getRequestOrgId());
1018
+ const access = await chatThreadAccessSql(ownerEmail, options.orgId === undefined ? getRequestOrgId() : options.orgId);
1019
+ const ownershipFilter = chatThreadOwnershipSql(ownerEmail, options.ownership);
1020
+ const select = chatThreadSummarySelect(ownerEmail);
969
1021
  const filters = [
970
1022
  access.sql,
971
1023
  `message_count > 0`,
972
1024
  `(title LIKE ? ESCAPE '!' OR preview LIKE ? ESCAPE '!' OR thread_data LIKE ? ESCAPE '!')`,
973
1025
  ];
974
- const args = [...access.args, pattern, pattern, pattern];
1026
+ const args = [
1027
+ ...select.args,
1028
+ ...access.args,
1029
+ pattern,
1030
+ pattern,
1031
+ pattern,
1032
+ ];
1033
+ if (ownershipFilter) {
1034
+ filters.push(ownershipFilter.sql);
1035
+ args.push(...ownershipFilter.args);
1036
+ }
975
1037
  if (!options.includeArchived) {
976
1038
  filters.push(`archived_at IS NULL`);
977
1039
  }
@@ -986,9 +1048,9 @@ export async function searchThreads(ownerEmail, query, limit = 50, options = {})
986
1048
  filters.push(`scope_type = ? AND scope_id = ?`);
987
1049
  args.push(options.scope.type, options.scope.id);
988
1050
  }
989
- args.push(limit);
1051
+ args.push(limit, options.offset ?? 0);
990
1052
  const { rows } = await client.execute({
991
- sql: `SELECT ${SUMMARY_COLUMNS} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC LIMIT ?`,
1053
+ sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
992
1054
  args,
993
1055
  });
994
1056
  return rows
@@ -10,6 +10,7 @@ const GIT_IDENTITY = {
10
10
  GIT_COMMITTER_NAME: "agent-native",
11
11
  GIT_COMMITTER_EMAIL: "noreply@agent-native.com",
12
12
  };
13
+ const KEEP_LINE_ENDINGS = ["-c", "core.autocrlf=false", "-c", "core.eol=lf"];
13
14
  export function resolveBaselineStore(appDir) {
14
15
  const resolved = realpath(path.resolve(appDir));
15
16
  const gitDir = findGitDir(resolved);
@@ -67,10 +68,15 @@ export function writeBaseline(store, sourceDir, slot, meta) {
67
68
  copyPlainTree(sourceDir, target);
68
69
  const indexFile = path.join(stage, "index");
69
70
  const env = { ...process.env, ...GIT_IDENTITY, GIT_INDEX_FILE: indexFile };
70
- git(store, ["--work-tree", stageRoot, "add", "-A", "--force", "."], {
71
- cwd: stageRoot,
72
- env,
73
- });
71
+ git(store, [
72
+ ...KEEP_LINE_ENDINGS,
73
+ "--work-tree",
74
+ stageRoot,
75
+ "add",
76
+ "-A",
77
+ "--force",
78
+ ".",
79
+ ], { cwd: stageRoot, env });
74
80
  const tree = git(store, ["write-tree"], { cwd: stageRoot, env }).trim();
75
81
  const ref = baselineRefName(store, slot);
76
82
  const parent = revParse(store, ref);
@@ -142,7 +148,14 @@ export function materializeBaseline(store, slot) {
142
148
  return null;
143
149
  }
144
150
  const tarPath = path.join(out, ".baseline.tar");
145
- const args = ["archive", "--format=tar", "-o", tarPath, ref];
151
+ const args = [
152
+ ...KEEP_LINE_ENDINGS,
153
+ "archive",
154
+ "--format=tar",
155
+ "-o",
156
+ tarPath,
157
+ ref,
158
+ ];
146
159
  if (store.prefix)
147
160
  args.push("--", store.prefix);
148
161
  git(store, args);
@@ -149,12 +149,13 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
149
149
  manual: [],
150
150
  keptLocal: [],
151
151
  };
152
- const paths = new Set([
152
+ const paths = Array.from(new Set([
153
153
  ...listMergeableFiles(baseDir),
154
154
  ...listMergeableFiles(theirsDir),
155
155
  ...listMergeableFiles(appDir),
156
- ]);
157
- for (const rel of Array.from(paths).sort()) {
156
+ ])).sort();
157
+ const normalized = crlfNormalizedPaths(appDir, paths);
158
+ for (const rel of paths) {
158
159
  const oursPath = path.join(appDir, rel);
159
160
  if (isLocalSymlink(oursPath)) {
160
161
  result.manual.push(`${rel} (local symlink)`);
@@ -162,7 +163,9 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
162
163
  }
163
164
  const base = readFileOrNull(path.join(baseDir, rel));
164
165
  const theirs = readFileOrNull(path.join(theirsDir, rel));
165
- const ours = readFileOrNull(oursPath);
166
+ const app = readAppFile(oursPath, normalized.has(rel) ? (base ?? theirs) : null);
167
+ const ours = app.content;
168
+ const writeOurs = (content) => writeFile(oursPath, app.crlf ? withCrlf(content) : content);
166
169
  if (equalBuffers(base, theirs))
167
170
  continue;
168
171
  if (theirs === null) {
@@ -193,7 +196,7 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
193
196
  continue;
194
197
  if (equalBuffers(ours, base)) {
195
198
  if (!options.dryRun)
196
- writeFile(oursPath, theirs);
199
+ writeOurs(theirs);
197
200
  result.updated.push(rel);
198
201
  continue;
199
202
  }
@@ -206,8 +209,9 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
206
209
  result.manual.push(`${rel} (merge failed)`);
207
210
  continue;
208
211
  }
209
- if (!options.dryRun)
210
- writeFile(oursPath, merged.content);
212
+ if (!options.dryRun && !equalBuffers(merged.content, ours)) {
213
+ writeOurs(merged.content);
214
+ }
211
215
  if (merged.conflicts > 0)
212
216
  result.conflicted.push(rel);
213
217
  else
@@ -475,19 +479,21 @@ async function statusCommand(target, flags, io) {
475
479
  try {
476
480
  const local = countLocallyModified(target.appDir, baseDir);
477
481
  lines.push(` locally modified files: ${local}`);
478
- const targetRef = flags.to ?? target.provenance.templateRef ?? latest;
479
- const theirs = await materializeForTarget(target, flags, targetRef);
480
482
  try {
481
- const changed = countUpstreamChanged(baseDir, theirs.dir);
482
- lines.push(` upstream-changed files: ${changed} (at ${theirs.ref})`);
483
+ const targetRef = flags.to ?? target.provenance.templateRef ?? latest;
484
+ const theirs = await materializeForTarget(target, flags, targetRef);
485
+ try {
486
+ const changed = countUpstreamChanged(baseDir, theirs.dir);
487
+ lines.push(` upstream-changed files: ${changed} (at ${theirs.ref})`);
488
+ }
489
+ finally {
490
+ fs.rmSync(theirs.dir, { recursive: true, force: true });
491
+ }
483
492
  }
484
- finally {
485
- fs.rmSync(theirs.dir, { recursive: true, force: true });
493
+ catch (err) {
494
+ lines.push(` upstream-changed files: unavailable (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
486
495
  }
487
496
  }
488
- catch (err) {
489
- lines.push(` upstream-changed files: unavailable (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
490
- }
491
497
  finally {
492
498
  fs.rmSync(baseDir, { recursive: true, force: true });
493
499
  }
@@ -648,13 +654,19 @@ function backfillProvenance(target, template, materialized) {
648
654
  shape: target.shape,
649
655
  };
650
656
  pkg["agent-native"] = agentNative;
651
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
657
+ const json = Buffer.from(`${JSON.stringify(pkg, null, 2)}\n`);
658
+ const app = readAppFile(pkgPath, json);
659
+ if (equalBuffers(app.content, json))
660
+ return;
661
+ fs.writeFileSync(pkgPath, app.crlf ? withCrlf(json) : json);
652
662
  }
653
663
  function countLocallyModified(appDir, baseDir) {
654
664
  let count = 0;
655
- for (const rel of listMergeableFiles(baseDir)) {
665
+ const paths = Array.from(listMergeableFiles(baseDir));
666
+ const normalized = crlfNormalizedPaths(appDir, paths);
667
+ for (const rel of paths) {
656
668
  const base = readFileOrNull(path.join(baseDir, rel));
657
- const ours = readFileOrNull(path.join(appDir, rel));
669
+ const ours = readAppFile(path.join(appDir, rel), normalized.has(rel) ? base : null).content;
658
670
  if (!equalBuffers(base, ours))
659
671
  count++;
660
672
  }
@@ -809,6 +821,75 @@ function equalBuffers(a, b) {
809
821
  return a === b;
810
822
  return a.equals(b);
811
823
  }
824
+ // CRLF that git normalizes on add is not a local edit.
825
+ function readAppFile(file, template) {
826
+ const content = readFileOrNull(file);
827
+ if (content === null ||
828
+ template === null ||
829
+ template.includes(0x0d) ||
830
+ !isTextBuffer(content) ||
831
+ !isTextBuffer(template)) {
832
+ return { content, crlf: false };
833
+ }
834
+ const text = content.toString("utf-8");
835
+ if (!text.includes("\r\n") || /\r(?!\n)/.test(text)) {
836
+ return { content, crlf: false };
837
+ }
838
+ return {
839
+ content: Buffer.from(text.replace(/\r\n/g, "\n")),
840
+ crlf: !/(^|[^\r])\n/.test(text),
841
+ };
842
+ }
843
+ // Paths whose CRLF git turns into LF on add; anywhere else a CR is content.
844
+ function crlfNormalizedPaths(appDir, rels) {
845
+ const normalized = new Set();
846
+ if (!resolveBaselineStore(appDir).gitDir)
847
+ return normalized;
848
+ const res = spawnSync("git", ["check-attr", "-z", "--stdin", "text", "crlf", "eol"], {
849
+ cwd: appDir,
850
+ input: rels.join("\0"),
851
+ encoding: "utf-8",
852
+ maxBuffer: 64 * 1024 * 1024,
853
+ });
854
+ if (res.status !== 0) {
855
+ throw new Error(`git check-attr failed in ${appDir}: ${res.stderr || res.error?.message}`);
856
+ }
857
+ const config = spawnSync("git", ["config", "--get", "core.autocrlf"], {
858
+ cwd: appDir,
859
+ encoding: "utf-8",
860
+ });
861
+ if (config.status !== 0 && config.status !== 1) {
862
+ throw new Error(`git config core.autocrlf failed in ${appDir}: ${config.stderr || config.error?.message}`);
863
+ }
864
+ const byConfig = ["true", "yes", "on", "1", "input"].includes(config.stdout.trim().toLowerCase());
865
+ const attrs = new Map();
866
+ const fields = res.stdout.split("\0");
867
+ for (let i = 0; i + 2 < fields.length; i += 3) {
868
+ attrs.set(fields[i], {
869
+ ...attrs.get(fields[i]),
870
+ [fields[i + 1]]: fields[i + 2],
871
+ });
872
+ }
873
+ for (const [rel, { text, crlf, eol }] of attrs) {
874
+ const byAttrs = conversion(text) ??
875
+ conversion(crlf) ??
876
+ (eol === "lf" || eol === "crlf" ? true : null);
877
+ if (byAttrs ?? byConfig)
878
+ normalized.add(rel);
879
+ }
880
+ return normalized;
881
+ }
882
+ // A text or crlf attribute as git reads it: convert, keep bytes, or no say.
883
+ function conversion(value) {
884
+ if (value === "set" || value === "auto" || value === "input")
885
+ return true;
886
+ return value === "unset" ? false : null;
887
+ }
888
+ function withCrlf(content) {
889
+ if (!isTextBuffer(content))
890
+ return content;
891
+ return Buffer.from(content.toString("utf-8").replace(/\r?\n/g, "\r\n"));
892
+ }
812
893
  function isTextBuffer(buf) {
813
894
  if (buf === null)
814
895
  return true;
@@ -17,7 +17,8 @@ export { AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, initiali
17
17
  export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions, normalizeAgentDynamicSuggestionsConfig, useAgentDynamicSuggestions, useAgentDynamicSuggestionsResult, type AgentDynamicSuggestionContext, type AgentDynamicSuggestionsConfig, type AgentDynamicSuggestionsOption, } from "../dynamic-suggestions.js";
18
18
  export { clearChatStorage } from "../chat/storage.js";
19
19
  export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, type AssistantChatHistoryDate, type AssistantChatHistoryVersion, } from "../chat/assistant-chat-history-version.js";
20
- export { useChatThreads, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
20
+ export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, } from "../use-chat-thread-list.js";
21
+ export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, type ChatThreadAccessBasis, type ChatThreadListPage, type ChatThreadListRow, type ChatThreadListPageRequest, type ChatThreadOwnership, type ChatThreadVisibility, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
21
22
  export { type AgentCallProgress, type ContentPart, } from "../sse-event-processor.js";
22
23
  export { useRunStuckDetection, useAbortRun, type RunStuckState, type UseRunStuckDetectionOptions, } from "../use-run-stuck-detection.js";
23
24
  export { AGENT_CHAT_HOME_HANDOFF_TTL_MS, AGENT_CHAT_VIEW_TRANSITION_CLASS, AGENT_CHAT_VIEW_TRANSITION_NAME, consumeAgentChatHomeHandoff, getAgentChatViewTransitionStyle, isAgentChatHomeHandoffActive, markAgentChatHomeHandoff, navigateWithAgentChatViewTransition, startAgentChatViewTransition, supportsAgentChatViewTransition, type AgentChatHomeHandoffOptions, type AgentChatViewTransition, type AgentChatViewTransitionOptions, } from "../chat-view-transition.js";
@@ -17,7 +17,8 @@ export { AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, initiali
17
17
  export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions, normalizeAgentDynamicSuggestionsConfig, useAgentDynamicSuggestions, useAgentDynamicSuggestionsResult, } from "../dynamic-suggestions.js";
18
18
  export { clearChatStorage } from "../chat/storage.js";
19
19
  export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, } from "../chat/assistant-chat-history-version.js";
20
- export { useChatThreads, } from "../use-chat-threads.js";
20
+ export { ChatThreadListConfigError, useChatThreadList, } from "../use-chat-thread-list.js";
21
+ export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, } from "../use-chat-threads.js";
21
22
  export { useRunStuckDetection, useAbortRun, } from "../use-run-stuck-detection.js";
22
23
  export { AGENT_CHAT_HOME_HANDOFF_TTL_MS, AGENT_CHAT_VIEW_TRANSITION_CLASS, AGENT_CHAT_VIEW_TRANSITION_NAME, consumeAgentChatHomeHandoff, getAgentChatViewTransitionStyle, isAgentChatHomeHandoffActive, markAgentChatHomeHandoff, navigateWithAgentChatViewTransition, startAgentChatViewTransition, supportsAgentChatViewTransition, } from "../chat-view-transition.js";
23
24
  export { useAgentChatHomeHandoff, useAgentChatHomeHandoffLinks, } from "../use-agent-chat-home-handoff.js";
@@ -1442,6 +1442,9 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1442
1442
  runId: input.runId,
1443
1443
  threadId: input.threadId,
1444
1444
  session,
1445
+ ...(state?.status !== "missing" && state?.startMessageId
1446
+ ? { startMessageId: state.startMessageId }
1447
+ : {}),
1445
1448
  turn: runAuthority
1446
1449
  ? turn
1447
1450
  : {
@@ -1520,6 +1523,9 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1520
1523
  runId,
1521
1524
  threadId,
1522
1525
  session,
1526
+ ...(start.messageId === undefined
1527
+ ? {}
1528
+ : { startMessageId: start.messageId }),
1523
1529
  turn,
1524
1530
  events: [],
1525
1531
  firstRetainedSequence: 1,
@@ -1550,6 +1556,15 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1550
1556
  ? findRun(threadId, runId)
1551
1557
  : undefined;
1552
1558
  if (existingRun) {
1559
+ if (start.messageId === undefined ||
1560
+ existingRun.startMessageId !== start.messageId) {
1561
+ await closeDiscardedTurnEvents(turn);
1562
+ throw new Error(`Duplicate AgentKit run id: ${runId}`);
1563
+ }
1564
+ if (turn.id !== undefined && existingRun.turn.id === turn.id) {
1565
+ await closeDiscardedTurnEvents(turn);
1566
+ return existingRun.runId;
1567
+ }
1553
1568
  // A duplicate queued submission is answered with the run that already
1554
1569
  // took it, replayed from its first event. This adapter reads that run
1555
1570
  // already, so the replay is drained rather than indexed a second time.
@@ -3125,6 +3140,7 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3125
3140
  });
3126
3141
  return openStartedRun(input.threadId, session, turn, turnMetadata, undefined, {
3127
3142
  attachReplayedRun: true,
3143
+ messageId: latestUserMessage?.id,
3128
3144
  });
3129
3145
  };
3130
3146
  if (!queuePromotionKey) {
@@ -3427,6 +3443,9 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3427
3443
  runId: nextRunId,
3428
3444
  threadId: input.threadId,
3429
3445
  session: run.session,
3446
+ ...(run.startMessageId === undefined
3447
+ ? {}
3448
+ : { startMessageId: run.startMessageId }),
3430
3449
  turn: nextTurn,
3431
3450
  events: [],
3432
3451
  firstRetainedSequence: 1,
@@ -3541,7 +3560,8 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3541
3560
  ...(attachments.length ? { attachments } : {}),
3542
3561
  abortSignal: readers.signal,
3543
3562
  });
3544
- const runId = await openStartedRun(input.threadId, session, turn, undefined, { continuedRunId: input.runId });
3563
+ const startMessageId = local?.startMessageId ?? state.startMessageId;
3564
+ const runId = await openStartedRun(input.threadId, session, turn, undefined, { continuedRunId: input.runId }, startMessageId === undefined ? {} : { messageId: startMessageId });
3545
3565
  return { runId, capabilities };
3546
3566
  };
3547
3567
  }
@@ -65,6 +65,7 @@ export type ServerRunState = {
65
65
  readonly status: "running" | "completed" | "truncated" | "errored" | "aborted";
66
66
  readonly runId: string;
67
67
  readonly turnId?: string;
68
+ readonly startMessageId?: string;
68
69
  /** Server clock, epoch ms. */
69
70
  readonly startedAt?: number;
70
71
  readonly dispatchMode?: string;
@@ -3459,10 +3459,15 @@ export function createAgentNativeChatRuntime(options = {}) {
3459
3459
  throw unreadable(`Agent chat run state is unreadable (run ${String(runId)}, status ${String(status)}).`, response.status);
3460
3460
  }
3461
3461
  const pendingConnectionRequestId = value?.pendingConnectionRequestId;
3462
+ const startMessageId = value?.startMessageId;
3462
3463
  if (input.includePendingConnectionRequest &&
3463
3464
  (!value || !Object.hasOwn(value, "pendingConnectionRequestId"))) {
3464
3465
  throw unreadable("Agent chat run state is unreadable (pending connection request was not returned).", response.status);
3465
3466
  }
3467
+ if (startMessageId !== undefined &&
3468
+ (typeof startMessageId !== "string" || !startMessageId.trim())) {
3469
+ throw unreadable("Agent chat run state is unreadable (start message ID is invalid).", response.status);
3470
+ }
3466
3471
  if (pendingConnectionRequestId !== undefined &&
3467
3472
  pendingConnectionRequestId !== null &&
3468
3473
  (typeof pendingConnectionRequestId !== "string" ||
@@ -3475,6 +3480,7 @@ export function createAgentNativeChatRuntime(options = {}) {
3475
3480
  ...(typeof value?.turnId === "string" && value.turnId
3476
3481
  ? { turnId: value.turnId }
3477
3482
  : {}),
3483
+ ...(typeof startMessageId === "string" ? { startMessageId } : {}),
3478
3484
  ...(typeof value?.startedAt === "number" &&
3479
3485
  Number.isFinite(value.startedAt)
3480
3486
  ? { startedAt: value.startedAt }
@@ -3,4 +3,5 @@ export { useAccessRequestDecisions, useAccessRequestReview, useResourceAccessReq
3
3
  export { useResourceAccessGate, type ResourceAccessGateController, type ResourceAccessGateOptions, type ResourceAccessGateRequest, type ResourceAccessRequestFailure, type ResourceAccessGateRole, type ResourceAccessGateState, type ResourceAccessGateStatus, } from "./useResourceAccessGate.js";
4
4
  export { useShareButtonController, type ShareButtonController, type ShareButtonControllerOptions, type ShareButtonOrgMember, type ShareButtonOrgMemberSearch, type ShareButtonRole, type ShareButtonShare, type ShareButtonSharesResponse, type ShareButtonVisibility, } from "./useShareButtonController.js";
5
5
  export { useShareDialogController, type ResourceShare, type ResourceSharesResponse, type ShareDialogController, type ShareDialogControllerOptions, type ShareDialogPerson, type ShareDialogTab, type ShareOption, type ShareRole, type ShareVisibility, } from "./useShareDialogController.js";
6
- export { createShareQueryKey, createShareQueryParams, extractShareErrorMessage, fetchOrgMemberPage, mergeOrgMembers, normalizeOrgMembers, optimisticallyUpdateShareCache, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, type OrgMemberPage, type ShareOrgMember, type ShareOrgMemberSearchResult, type ShareQueryKey, type ShareQueryParams, } from "./share-controller-helpers.js";
6
+ export { ShareVisibilityNotSavedError, useShareVisibilityTransition, visibilityNeedsConfirmation, type ShareVisibilityTransition, type ShareVisibilityTransitionOptions, type ShareVisibilityTransitionRequest, } from "./useShareVisibilityTransition.js";
7
+ export { createShareQueryKey, createShareQueryParams, extractShareErrorMessage, fetchOrgMemberPage, mergeOrgMembers, normalizeOrgMembers, notifyShareChanged, optimisticallyUpdateShareCache, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, type OrgMemberPage, type ShareOrgMember, type ShareOrgMemberSearchResult, type ShareQueryKey, type ShareQueryParams, } from "./share-controller-helpers.js";
@@ -3,4 +3,5 @@ export { useAccessRequestDecisions, useAccessRequestReview, useResourceAccessReq
3
3
  export { useResourceAccessGate, } from "./useResourceAccessGate.js";
4
4
  export { useShareButtonController, } from "./useShareButtonController.js";
5
5
  export { useShareDialogController, } from "./useShareDialogController.js";
6
- export { createShareQueryKey, createShareQueryParams, extractShareErrorMessage, fetchOrgMemberPage, mergeOrgMembers, normalizeOrgMembers, optimisticallyUpdateShareCache, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, } from "./share-controller-helpers.js";
6
+ export { ShareVisibilityNotSavedError, useShareVisibilityTransition, visibilityNeedsConfirmation, } from "./useShareVisibilityTransition.js";
7
+ export { createShareQueryKey, createShareQueryParams, extractShareErrorMessage, fetchOrgMemberPage, mergeOrgMembers, normalizeOrgMembers, notifyShareChanged, optimisticallyUpdateShareCache, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, } from "./share-controller-helpers.js";