@agent-native/core 0.165.1 → 0.165.3

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 (68) hide show
  1. package/corpus/templates/analytics/CHANGELOG.md +37 -3
  2. package/corpus/templates/analytics/changelog/2026-08-20-staged-data-limit-recovery.md +6 -0
  3. package/corpus/templates/assets/CHANGELOG.md +36 -2
  4. package/corpus/templates/brain/CHANGELOG.md +33 -3
  5. package/corpus/templates/calendar/CHANGELOG.md +45 -2
  6. package/corpus/templates/chat/CHANGELOG.md +29 -3
  7. package/corpus/templates/clips/CHANGELOG.md +55 -2
  8. package/corpus/templates/clips/app/i18n/en-US.ts +6 -11
  9. package/corpus/templates/clips/app/root.tsx +2 -1
  10. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +3 -3
  11. package/corpus/templates/clips/app/routes/download.tsx +111 -17
  12. package/corpus/templates/clips/changelog/2026-08-20-cross-tab-recording-controls.md +6 -0
  13. package/corpus/templates/clips/changelog/2026-08-20-overlay-follow-review-fixes.md +6 -0
  14. package/corpus/templates/clips/changelog/2026-08-20-provider-key-recovery.md +6 -0
  15. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +29 -46
  16. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +1 -1
  17. package/corpus/templates/clips/chrome-extension/public/manifest.json +9 -2
  18. package/corpus/templates/clips/chrome-extension/src/background.ts +65 -46
  19. package/corpus/templates/clips/chrome-extension/src/content-script.ts +25 -17
  20. package/corpus/templates/clips/chrome-extension/src/overlay-follow.ts +25 -0
  21. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +109 -46
  22. package/corpus/templates/content/CHANGELOG.md +45 -2
  23. package/corpus/templates/crm/CHANGELOG.md +28 -16
  24. package/corpus/templates/design/CHANGELOG.md +54 -2
  25. package/corpus/templates/dispatch/CHANGELOG.md +51 -2
  26. package/corpus/templates/factory/CHANGELOG.md +28 -2
  27. package/corpus/templates/forms/CHANGELOG.md +39 -3
  28. package/corpus/templates/macros/CHANGELOG.md +30 -2
  29. package/corpus/templates/mail/CHANGELOG.md +34 -3
  30. package/corpus/templates/plan/CHANGELOG.md +34 -3
  31. package/corpus/templates/slides/CHANGELOG.md +73 -2
  32. package/corpus/templates/tasks/CHANGELOG.md +35 -0
  33. package/corpus/templates/tasks/app/root.tsx +8 -1
  34. package/dist/changelog/parse.d.ts +12 -4
  35. package/dist/changelog/parse.js +61 -23
  36. package/dist/cli/changelog.js +9 -11
  37. package/dist/client/blocks/library/diagram.js +1 -1
  38. package/dist/client/chat/run-recovery.js +8 -3
  39. package/dist/collab/routes.d.ts +1 -1
  40. package/dist/deploy/workspace-deploy.js +7 -0
  41. package/dist/mcp-client/oauth-routes.d.ts +1 -0
  42. package/dist/mcp-client/oauth-routes.js +15 -2
  43. package/dist/mcp-client/remote-store.js +83 -43
  44. package/dist/notifications/routes.d.ts +3 -3
  45. package/dist/observability/routes.d.ts +5 -5
  46. package/dist/progress/routes.d.ts +1 -1
  47. package/dist/provider-api/actions/provider-api.js +16 -4
  48. package/dist/resources/handlers.d.ts +2 -2
  49. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  50. package/dist/server/google-oauth-credentials.js +14 -1
  51. package/dist/server/realtime-token.d.ts +1 -1
  52. package/dist/server/transcribe-voice.d.ts +1 -1
  53. package/dist/settings/index.d.ts +1 -1
  54. package/dist/settings/index.js +1 -1
  55. package/dist/settings/store.d.ts +7 -1
  56. package/dist/settings/store.js +25 -3
  57. package/dist/settings/user-settings.d.ts +2 -0
  58. package/dist/settings/user-settings.js +65 -2
  59. package/dist/templates/chat/CHANGELOG.md +29 -3
  60. package/docs/content/deployment.mdx +89 -14
  61. package/package.json +3 -3
  62. package/src/templates/chat/CHANGELOG.md +29 -3
  63. package/corpus/templates/.retired/issues/package.json +0 -7
  64. package/corpus/templates/.retired/meeting-notes/package.json +0 -7
  65. package/corpus/templates/.retired/recruiting/package.json +0 -7
  66. package/corpus/templates/.retired/scheduling/package.json +0 -7
  67. package/corpus/templates/.retired/voice/package.json +0 -7
  68. package/corpus/templates/voice/.agents/skills/upgrade-agent-native/SKILL.md +0 -87
@@ -21,8 +21,8 @@
21
21
  */
22
22
  import { createHash } from "node:crypto";
23
23
  import { writeAppSecret, readAppSecret, deleteAppSecret, } from "../secrets/storage.js";
24
- import { getOrgSetting, putOrgSetting, deleteOrgSetting, } from "../settings/org-settings.js";
25
- import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
24
+ import { getOrgSetting, mutateOrgSetting } from "../settings/org-settings.js";
25
+ import { getUserSetting, mutateUserSetting, } from "../settings/user-settings.js";
26
26
  import { getMcpOAuthAccessToken, revokeMcpOAuthCredentials, saveMcpOAuthCredentials, } from "./oauth-client.js";
27
27
  import { validateRemoteUrl } from "./remote-url.js";
28
28
  export { validateRemoteUrl } from "./remote-url.js";
@@ -107,20 +107,21 @@ function sanitiseOrgId(orgId) {
107
107
  return orgId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
108
108
  }
109
109
  async function readList(scope, scopeId) {
110
- const raw = scope === "user"
110
+ return parseServerList(scope === "user"
111
111
  ? await getUserSetting(scopeId, SETTINGS_KEY)
112
- : await getOrgSetting(scopeId, SETTINGS_KEY);
112
+ : await getOrgSetting(scopeId, SETTINGS_KEY));
113
+ }
114
+ function parseServerList(raw) {
113
115
  if (!raw || !Array.isArray(raw.servers))
114
116
  return [];
115
117
  return raw.servers.filter((s) => s && typeof s.id === "string" && typeof s.url === "string");
116
118
  }
117
- async function writeList(scope, scopeId, servers) {
118
- if (scope === "user") {
119
- await putUserSetting(scopeId, SETTINGS_KEY, { servers });
120
- }
121
- else {
122
- await putOrgSetting(scopeId, SETTINGS_KEY, { servers });
123
- }
119
+ async function mutateServerList(scope, scopeId, updater) {
120
+ const mutate = scope === "user" ? mutateUserSetting : mutateOrgSetting;
121
+ const next = await mutate(scopeId, SETTINGS_KEY, async (raw) => ({
122
+ servers: await updater(parseServerList(raw)),
123
+ }));
124
+ return parseServerList(next);
124
125
  }
125
126
  export async function listRemoteServers(scope, scopeId) {
126
127
  return readList(scope, scopeId);
@@ -228,6 +229,7 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
228
229
  ...credentials,
229
230
  serverUrl: credentialResource.url.toString(),
230
231
  };
232
+ let updated;
231
233
  try {
232
234
  await saveMcpOAuthCredentials({
233
235
  key: nextSecretKey,
@@ -235,21 +237,22 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
235
237
  scopeId,
236
238
  credentials: nextCredentials,
237
239
  });
238
- const server = {
239
- ...current,
240
- url: nextCredentials.serverUrl,
241
- oauthSecretKey: nextSecretKey,
242
- };
243
- const next = [...existing];
244
- next[index] = server;
245
- await writeList(scope, scopeId, next);
246
- await revokeMcpOAuthCredentials({
247
- key: current.oauthSecretKey,
248
- scope,
249
- scopeId,
250
- serverUrl: current.url,
251
- }).catch(() => undefined);
252
- return { ok: true, server };
240
+ updated = await mutateServerList(scope, scopeId, (servers) => {
241
+ const latestIndex = servers.findIndex((server) => server.id === serverId);
242
+ const latest = latestIndex >= 0 ? servers[latestIndex] : undefined;
243
+ if (!latest ||
244
+ latest.oauthSecretKey !== current.oauthSecretKey ||
245
+ latest.url !== current.url) {
246
+ throw new Error("MCP server changed while reconnecting");
247
+ }
248
+ const next = [...servers];
249
+ next[latestIndex] = {
250
+ ...latest,
251
+ url: nextCredentials.serverUrl,
252
+ oauthSecretKey: nextSecretKey,
253
+ };
254
+ return next;
255
+ });
253
256
  }
254
257
  catch (err) {
255
258
  await revokeMcpOAuthCredentials({
@@ -263,6 +266,18 @@ export async function replaceOAuthRemoteServer(scope, scopeId, serverId, credent
263
266
  error: `Failed to replace MCP OAuth credentials: ${err?.message ?? err}`,
264
267
  };
265
268
  }
269
+ // The settings row now points at the replacement grant. Old-grant cleanup
270
+ // is best effort and must not roll back or invalidate the committed row.
271
+ await revokeMcpOAuthCredentials({
272
+ key: current.oauthSecretKey,
273
+ scope,
274
+ scopeId,
275
+ serverUrl: current.url,
276
+ }).catch(() => undefined);
277
+ const server = updated.find((candidate) => candidate.id === serverId);
278
+ return server
279
+ ? { ok: true, server }
280
+ : { ok: false, error: "MCP server was not found" };
266
281
  }
267
282
  export async function addFirstPartyRemoteServer(orgId, input) {
268
283
  const trust = await isFirstPartyRemoteEndpointTrusted(orgId, input.appId, input.url);
@@ -327,7 +342,24 @@ async function addRemoteServerInternal(scope, scopeId, input) {
327
342
  description: input.description?.trim() || undefined,
328
343
  createdAt: Date.now(),
329
344
  };
330
- await writeList(scope, scopeId, [...existing, server]);
345
+ try {
346
+ await mutateServerList(scope, scopeId, (servers) => {
347
+ if (servers.some((candidate) => candidate.name === name)) {
348
+ throw new Error(`A server named "${name}" already exists`);
349
+ }
350
+ return [...servers, server];
351
+ });
352
+ }
353
+ catch (err) {
354
+ if (headerSecretKey) {
355
+ await deleteAppSecret({
356
+ key: headerSecretKey,
357
+ scope: toSecretScope(scope),
358
+ scopeId,
359
+ }).catch(() => undefined);
360
+ }
361
+ return { ok: false, error: err?.message ?? String(err) };
362
+ }
331
363
  return { ok: true, server };
332
364
  }
333
365
  function normalizeEndpointPath(pathname) {
@@ -396,9 +428,29 @@ export async function isFirstPartyRemoteEndpointTrusted(orgId, appId, endpoint)
396
428
  }
397
429
  export async function removeRemoteServer(scope, scopeId, id) {
398
430
  const existing = await readList(scope, scopeId);
399
- const removed = existing.find((s) => s.id === id);
400
- const next = existing.filter((s) => s.id !== id);
401
- if (next.length === existing.length)
431
+ const expected = existing.find((s) => s.id === id);
432
+ if (!expected)
433
+ return false;
434
+ let removed;
435
+ try {
436
+ await mutateServerList(scope, scopeId, (servers) => {
437
+ removed = undefined;
438
+ const latest = servers.find((server) => server.id === id);
439
+ if (!latest)
440
+ return servers;
441
+ if (latest.oauthSecretKey !== expected.oauthSecretKey) {
442
+ throw new Error(`MCP OAuth credentials changed while removing ${expected.name}`);
443
+ }
444
+ removed = latest;
445
+ return servers.filter((server) => server.id !== id);
446
+ });
447
+ }
448
+ catch (err) {
449
+ // eslint-disable-next-line no-console
450
+ console.warn(`[mcp-client] Failed to remove MCP server ${expected.name}: ${err?.message ?? err}`);
451
+ return false;
452
+ }
453
+ if (!removed)
402
454
  return false;
403
455
  if (removed?.oauthSecretKey) {
404
456
  try {
@@ -409,8 +461,7 @@ export async function removeRemoteServer(scope, scopeId, id) {
409
461
  serverUrl: removed.url,
410
462
  });
411
463
  if (result.local === "replaced") {
412
- console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server was kept so the newer connection remains manageable.`);
413
- return false;
464
+ console.warn(`[mcp-client] MCP OAuth credentials changed while removing ${removed.name}; the server row was already removed.`);
414
465
  }
415
466
  if (result.remote === "failed") {
416
467
  console.warn(`[mcp-client] MCP OAuth revocation failed for ${removed.name}; local credentials were removed.`);
@@ -421,17 +472,6 @@ export async function removeRemoteServer(scope, scopeId, id) {
421
472
  console.warn(`[mcp-client] Failed to delete MCP OAuth credentials ${removed.oauthSecretKey}: ${err?.message ?? err}`);
422
473
  }
423
474
  }
424
- if (next.length === 0) {
425
- if (scope === "user") {
426
- await deleteUserSetting(scopeId, SETTINGS_KEY);
427
- }
428
- else {
429
- await deleteOrgSetting(scopeId, SETTINGS_KEY);
430
- }
431
- }
432
- else {
433
- await writeList(scope, scopeId, next);
434
- }
435
475
  // Best-effort: drop the encrypted-headers secret too. Errors are logged
436
476
  // but don't fail the deletion — the settings row is already gone, so a
437
477
  // dangling secret is harmless (it just can't be read back).
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- error?: undefined;
17
16
  ok?: undefined;
17
+ error?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- error?: undefined;
22
21
  ok?: undefined;
22
+ error?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- error?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
- error?: undefined;
45
- ok?: undefined;
46
44
  summary: import("./types.js").TraceSummary;
47
45
  spans: import("./types.js").TraceSpan[];
48
46
  id?: undefined;
49
- } | {
50
47
  error?: undefined;
51
48
  ok?: undefined;
49
+ } | {
52
50
  summary?: undefined;
53
51
  spans?: undefined;
54
52
  id: string;
55
- } | {
53
+ error?: undefined;
56
54
  ok?: undefined;
55
+ } | {
57
56
  summary?: undefined;
58
57
  spans?: undefined;
59
58
  id?: undefined;
60
59
  error: any;
60
+ ok?: undefined;
61
61
  } | {
62
- error?: undefined;
63
62
  summary?: undefined;
64
63
  spans?: undefined;
65
64
  id?: undefined;
65
+ error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -193,6 +193,13 @@ export function createProviderApiDocsSchema(providerSchema = z.string().min(1))
193
193
  search: WebContentSearchSchema.optional().describe("Optional post-fetch search over extracted content by default. Supports query, queries, terms, regex, source, maxMatches, and contextChars."),
194
194
  });
195
195
  }
196
+ const STAGED_DATASET_CAP_ERROR = /Staged dataset (?:byte )?cap exceeded/i;
197
+ function rethrowStagingError(error) {
198
+ if (error instanceof Error && STAGED_DATASET_CAP_ERROR.test(error.message)) {
199
+ throw new Error(`${error.message} Recover by deleting older staged datasets with list-staged-datasets/delete-staged-dataset, or switch this request to saveToFile / a smaller staged result before trying again.`, { cause: error });
200
+ }
201
+ throw error;
202
+ }
196
203
  export function createProviderApiRequestAction(runtime, options = {}) {
197
204
  const schema = options.schema ?? createProviderApiRequestSchema();
198
205
  return defineAction({
@@ -244,10 +251,15 @@ export function createProviderApiRequestAction(runtime, options = {}) {
244
251
  if (!ownerEmail) {
245
252
  throw new Error("No authenticated context for provider API staging.");
246
253
  }
247
- return stagingExecuteRequest(args, runtime.executeRequest, {
248
- appId: options.appId,
249
- ownerEmail,
250
- });
254
+ try {
255
+ return await stagingExecuteRequest(args, runtime.executeRequest, {
256
+ appId: options.appId,
257
+ ownerEmail,
258
+ });
259
+ }
260
+ catch (error) {
261
+ rethrowStagingError(error);
262
+ }
251
263
  }
252
264
  return runtime.executeRequest(args);
253
265
  },
@@ -51,8 +51,8 @@ export declare function handleDeleteResource(event: any): Promise<{
51
51
  error: string;
52
52
  ok?: undefined;
53
53
  } | {
54
- error?: undefined;
55
54
  ok: boolean;
55
+ error?: undefined;
56
56
  }>;
57
57
  /** POST /_agent-native/resources/upload — upload a file as a resource */
58
58
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
@@ -73,10 +73,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
73
73
  runId: string | null;
74
74
  expiresAt: number | null;
75
75
  metadata: string | null;
76
- error?: undefined;
77
76
  url: string;
78
77
  provider: string;
79
78
  storageSetupRequired?: undefined;
79
+ error?: undefined;
80
80
  } | {
81
81
  error: string;
82
82
  storageSetupRequired: boolean;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -57,7 +57,20 @@ export async function resolveGoogleProviderCredentialCandidatesWithReader(option
57
57
  * remain the backwards-compatible provider OAuth credentials.
58
58
  */
59
59
  export function resolveGoogleSignInCredentials() {
60
- return (readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET") ?? readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"));
60
+ const signIn = readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET");
61
+ const provider = readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
62
+ // Both pairs configured for different clients means one of them is dead
63
+ // config that nothing will ever read. Silence here cost a fleet-wide beta
64
+ // sign-in outage: GOOGLE_CLIENT_SECRET was repaired and verified while the
65
+ // callback kept using a stale GOOGLE_SIGN_IN_CLIENT_SECRET, so the repaired
66
+ // variable looked correct and changed nothing. Name the loser explicitly.
67
+ if (signIn && provider && signIn.clientId !== provider.clientId) {
68
+ console.warn("[agent-native][google-oauth] GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_CLIENT_ID " +
69
+ "are set to different Google clients. Sign-in uses GOOGLE_SIGN_IN_CLIENT_ID; " +
70
+ "GOOGLE_CLIENT_ID/SECRET are ignored for sign-in. Editing them will not " +
71
+ "change sign-in behaviour.");
72
+ }
73
+ return signIn ?? provider;
61
74
  }
62
75
  export function hasGoogleSignInCredentials() {
63
76
  return resolveGoogleSignInCredentials() !== null;
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -1,4 +1,4 @@
1
- export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
1
+ export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
2
2
  export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
3
3
  export { readSetting, writeSetting, removeSetting } from "./script-helpers.js";
4
4
  export { getUserSetting, putUserSetting, deleteUserSetting, } from "./user-settings.js";
@@ -1,5 +1,5 @@
1
1
  // Store
2
- export { getSetting, mutateSetting, putSetting, deleteSetting, getAllSettings, getSettingsEmitter, } from "./store.js";
2
+ export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, } from "./store.js";
3
3
  // H3 route handlers
4
4
  export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
5
5
  // Script helpers
@@ -1,7 +1,11 @@
1
1
  import type { EventEmitter } from "node:events";
2
2
  export declare function getSettingsEmitter(): EventEmitter;
3
3
  export declare function ensureTable(): Promise<void>;
4
- export declare function getSetting(key: string): Promise<Record<string, unknown> | null>;
4
+ export interface StoreReadOptions {
5
+ /** Skip the per-request snapshot when a cross-request race must be checked. */
6
+ bypassCache?: boolean;
7
+ }
8
+ export declare function getSetting(key: string, options?: StoreReadOptions): Promise<Record<string, unknown> | null>;
5
9
  export interface StoreWriteOptions {
6
10
  /** Tag identifying who initiated this write (e.g. a tab ID). */
7
11
  requestSource?: string;
@@ -16,6 +20,8 @@ export interface StoreWriteOptions {
16
20
  export declare function mutateSetting(key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
17
21
  export declare function putSetting(key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
18
22
  export declare function deleteSetting(key: string, options?: StoreWriteOptions): Promise<boolean>;
23
+ /** Delete a setting only when its stored value still matches the inspected value. */
24
+ export declare function deleteSettingIfValue(key: string, expected: Record<string, unknown>, options?: StoreWriteOptions): Promise<boolean>;
19
25
  /**
20
26
  * Delete every setting whose key starts with `prefix`. Returns the number of
21
27
  * rows removed. Used when an owning entity is deleted (e.g. an organization
@@ -115,9 +115,9 @@ export async function ensureTable() {
115
115
  }
116
116
  return _initPromise;
117
117
  }
118
- export async function getSetting(key) {
118
+ export async function getSetting(key, options) {
119
119
  const cache = requestSettingsCache();
120
- if (cache?.has(key)) {
120
+ if (!options?.bypassCache && cache?.has(key)) {
121
121
  const cached = cache.get(key);
122
122
  return cached == null ? null : JSON.parse(cached);
123
123
  }
@@ -129,7 +129,8 @@ export async function getSetting(key) {
129
129
  args: [key],
130
130
  });
131
131
  const raw = rows.length === 0 ? null : rows[0].value;
132
- cache?.set(key, raw);
132
+ if (!options?.bypassCache)
133
+ cache?.set(key, raw);
133
134
  return raw == null ? null : JSON.parse(raw);
134
135
  }
135
136
  const SETTINGS_MUTATION_ATTEMPTS = 25;
@@ -221,6 +222,27 @@ export async function deleteSetting(key, options) {
221
222
  }
222
223
  return false;
223
224
  }
225
+ /** Delete a setting only when its stored value still matches the inspected value. */
226
+ export async function deleteSettingIfValue(key, expected, options) {
227
+ await ensureTable();
228
+ const client = getDbExec();
229
+ const table = settingsTable();
230
+ const result = await client.execute({
231
+ sql: `DELETE FROM ${table} WHERE key = ? AND value = ?`,
232
+ args: [key, JSON.stringify(expected)],
233
+ });
234
+ if (result.rowsAffected === 0)
235
+ return false;
236
+ requestSettingsCache()?.set(key, null);
237
+ invalidateRequestAllSettings();
238
+ settingsEmitter().emit("settings", {
239
+ source: "settings",
240
+ type: "delete",
241
+ key,
242
+ ...(options?.requestSource && { requestSource: options.requestSource }),
243
+ });
244
+ return true;
245
+ }
224
246
  /**
225
247
  * Delete every setting whose key starts with `prefix`. Returns the number of
226
248
  * rows removed. Used when an owning entity is deleted (e.g. an organization
@@ -12,5 +12,7 @@ import { type StoreWriteOptions } from "./store.js";
12
12
  export declare function getUserSetting(email: string, key: string): Promise<Record<string, unknown> | null>;
13
13
  /** Write a user-scoped setting. Always writes to the prefixed key. */
14
14
  export declare function putUserSetting(email: string, key: string, value: Record<string, unknown>, options?: StoreWriteOptions): Promise<void>;
15
+ /** Atomically derive and persist one user-scoped setting. */
16
+ export declare function mutateUserSetting(email: string, key: string, updater: (current: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>, options?: StoreWriteOptions): Promise<Record<string, unknown>>;
15
17
  /** Delete a user-scoped setting. */
16
18
  export declare function deleteUserSetting(email: string, key: string, options?: StoreWriteOptions): Promise<boolean>;
@@ -7,7 +7,7 @@
7
7
  * No global fallback — each user starts with a clean slate. This
8
8
  * prevents one user's private data from leaking to other users.
9
9
  */
10
- import { getSetting, putSetting, deleteSetting, } from "./store.js";
10
+ import { getSetting, mutateSetting, putSetting, deleteSettingIfValue, } from "./store.js";
11
11
  function userKey(email, key) {
12
12
  return `u:${email.trim().toLowerCase()}:${key}`;
13
13
  }
@@ -31,7 +31,70 @@ export async function getUserSetting(email, key) {
31
31
  export async function putUserSetting(email, key, value, options) {
32
32
  return putSetting(userKey(email, key), value, options);
33
33
  }
34
+ /** Atomically derive and persist one user-scoped setting. */
35
+ export async function mutateUserSetting(email, key, updater, options) {
36
+ const normalized = userKey(email, key);
37
+ const legacy = legacyUserKey(email, key);
38
+ let migratedLegacy = false;
39
+ let migratedLegacyValue = null;
40
+ const result = await mutateSetting(normalized, async (current) => {
41
+ if (current !== null) {
42
+ migratedLegacy = false;
43
+ migratedLegacyValue = null;
44
+ return updater(current);
45
+ }
46
+ const legacyCurrent = legacy === normalized
47
+ ? null
48
+ : await getSetting(legacy, { bypassCache: true });
49
+ migratedLegacy = legacyCurrent !== null;
50
+ migratedLegacyValue = legacyCurrent;
51
+ return updater(legacyCurrent);
52
+ }, options);
53
+ if (!migratedLegacy)
54
+ return result;
55
+ // If the legacy row disappeared after the updater read it, a concurrent
56
+ // delete won the race. Remove only our exact canonical write; never delete
57
+ // a newer canonical value from another writer.
58
+ if (!migratedLegacyValue ||
59
+ !(await deleteSettingIfValue(legacy, migratedLegacyValue, options))) {
60
+ if (await getSetting(legacy, { bypassCache: true }))
61
+ return result;
62
+ const removed = await deleteSettingIfValue(normalized, result, options);
63
+ if (!removed) {
64
+ // The canonical write committed successfully. A concurrent canonical
65
+ // writer may have replaced it after the legacy row disappeared; keep
66
+ // the committed value visible instead of making callers revoke side
67
+ // effects for a write that is already persisted.
68
+ return result;
69
+ }
70
+ throw new Error("User setting was deleted while migrating its legacy key");
71
+ }
72
+ return result;
73
+ }
34
74
  /** Delete a user-scoped setting. */
35
75
  export async function deleteUserSetting(email, key, options) {
36
- return deleteSetting(userKey(email, key), options);
76
+ const normalized = userKey(email, key);
77
+ const legacy = legacyUserKey(email, key);
78
+ const normalizedCurrent = await getSetting(normalized, {
79
+ bypassCache: true,
80
+ });
81
+ if (legacy === normalized) {
82
+ return normalizedCurrent === null
83
+ ? false
84
+ : deleteSettingIfValue(normalized, normalizedCurrent, options);
85
+ }
86
+ const legacyCurrent = await getSetting(legacy, { bypassCache: true });
87
+ // Retire the fallback row first. If the canonical delete fails, the
88
+ // remaining canonical value still wins reads instead of resurrecting legacy
89
+ // data after a partial delete; a retry can safely finish the operation.
90
+ const deletedLegacy = legacyCurrent === null
91
+ ? false
92
+ : await deleteSettingIfValue(legacy, legacyCurrent, options);
93
+ const normalizedAfterLegacyCleanup = normalizedCurrent === null && deletedLegacy
94
+ ? await getSetting(normalized, { bypassCache: true })
95
+ : normalizedCurrent;
96
+ const deletedNormalized = normalizedAfterLegacyCleanup === null
97
+ ? false
98
+ : await deleteSettingIfValue(normalized, normalizedAfterLegacyCleanup, options);
99
+ return deletedNormalized || deletedLegacy;
37
100
  }
@@ -3,8 +3,34 @@
3
3
  All notable user-facing changes to Chat are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new").
5
5
 
6
- ## 2026-06-23
6
+ Older updates live in [the changelog folder](./changelog/) and are included in the in-app "What's new" view.
7
7
 
8
- ### Added
8
+ ## 2026-08-11
9
9
 
10
- - See what's new right inside Chat — a changelog now lives in the command menu (Cmd+K).
10
+ ### Fixed
11
+
12
+ - Chrome no longer offers to install Chat as a desktop app.
13
+
14
+ ## 2026-08-10
15
+
16
+ ### Improved
17
+
18
+ - Full-page chat now uses the available width up to 1000px for more comfortable prompts and responses.
19
+
20
+ ## 2026-07-31
21
+
22
+ ### Improved
23
+
24
+ - Clicking the Agent Native logo now toggles the app sidebar.
25
+
26
+ ## 2026-07-29
27
+
28
+ ### Improved
29
+
30
+ - Sidebar footers now keep Feedback, Search, and Collapse together without a separate language shortcut.
31
+
32
+ ## 2026-07-25
33
+
34
+ ### Improved
35
+
36
+ - Settings navigation now keeps Manage agent as a dedicated linked destination at the bottom.