@agent-native/core 0.168.7 → 0.168.10

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/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/mutate-dashboard.ts +8 -1
  3. package/corpus/templates/calendar/actions/delete-event.ts +34 -18
  4. package/corpus/templates/calendar/actions/delete-events.ts +13 -1
  5. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +22 -3
  6. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +19 -0
  7. package/corpus/templates/calendar/server/lib/google-api.ts +18 -1
  8. package/corpus/templates/clips/actions/finalize-meeting.ts +16 -0
  9. package/corpus/templates/clips/actions/list-meetings.ts +1 -0
  10. package/corpus/templates/clips/actions/sync-calendars.ts +10 -29
  11. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +28 -12
  12. package/corpus/templates/clips/desktop/src/lib/meeting-stop.ts +16 -0
  13. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +19 -9
  14. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +1 -0
  15. package/corpus/templates/content/app/global.css +5 -0
  16. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-slash-production-regression-shape.md +492 -0
  17. package/corpus/templates/content/docs/solutions/2026-08-20-code-block-surface-background-shape.md +273 -0
  18. package/corpus/templates/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  19. package/corpus/templates/factory/AGENTS.md +12 -22
  20. package/corpus/templates/factory/README.md +11 -8
  21. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  22. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  23. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  24. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  25. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  26. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  27. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  28. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  29. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  30. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  31. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  32. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  33. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  34. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  35. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  36. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  37. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  38. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  39. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  40. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  41. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  44. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  45. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  46. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  47. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  48. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  49. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  50. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  51. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  52. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  53. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  54. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  55. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  56. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  57. package/corpus/templates/factory/server/db/schema.ts +18 -3
  58. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  59. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  60. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  61. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  62. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  63. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  64. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  65. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  66. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  67. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  68. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  69. package/dist/cli/native-dependencies.js +4 -1
  70. package/dist/client/AgentPanel.d.ts +5 -1
  71. package/dist/client/AgentPanel.js +9 -4
  72. package/dist/client/agent-chat/index.d.ts +1 -1
  73. package/dist/client/agent-chat/index.js +1 -1
  74. package/dist/client/chat-first/apps-rail.js +6 -6
  75. package/dist/client/chat-first/primary-nav.js +14 -6
  76. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  77. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  78. package/dist/client/integrations/index.d.ts +1 -1
  79. package/dist/client/integrations/index.js +1 -1
  80. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  81. package/dist/collab/struct-routes.d.ts +1 -1
  82. package/dist/deploy/build.js +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/observability/routes.d.ts +3 -3
  85. package/dist/progress/routes.d.ts +1 -1
  86. package/dist/secrets/routes.d.ts +6 -6
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  90. package/dist/shared/mcp-embed-headers.js +10 -3
  91. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8496
34
+ - template files: 8504
@@ -49,6 +49,13 @@ const mutationTargetSchema = {
49
49
  .describe("Where in rowNumber to place the panel. Defaults to end."),
50
50
  };
51
51
 
52
+ const insertPanelSchema = z
53
+ .record(z.string(), z.unknown())
54
+ .refine(
55
+ (panel) => typeof panel.id === "string" && panel.id.trim().length > 0,
56
+ { message: "panel.id must be a non-empty string" },
57
+ );
58
+
52
59
  const mutationOperationSchema = z.discriminatedUnion("op", [
53
60
  z.object({
54
61
  op: z.literal("movePanels"),
@@ -72,7 +79,7 @@ const mutationOperationSchema = z.discriminatedUnion("op", [
72
79
  }),
73
80
  z.object({
74
81
  op: z.literal("insertPanel"),
75
- panel: z.record(z.string(), z.unknown()),
82
+ panel: insertPanelSchema,
76
83
  ...mutationTargetSchema,
77
84
  }),
78
85
  z.object({
@@ -5,6 +5,7 @@ import {
5
5
  normalizeGuestNotificationMessage,
6
6
  sendEventGuestNotificationNote,
7
7
  } from "../server/lib/event-guest-notifications.js";
8
+ import { isGoogleNotFoundError } from "../server/lib/google-api.js";
8
9
  import * as googleCalendar from "../server/lib/google-calendar.js";
9
10
  import {
10
11
  cliBoolean,
@@ -71,25 +72,39 @@ export default defineAction({
71
72
  ? "none"
72
73
  : (args.sendUpdates ?? (shouldNotifyGuests ? "all" : "none")),
73
74
  };
74
- const eventForNotification = shouldNotifyGuests
75
- ? await googleCalendar.getEvent(googleEventId, {
76
- ownerEmail,
77
- accountEmail,
78
- })
79
- : undefined;
75
+ let eventForNotification;
76
+ try {
77
+ eventForNotification = shouldNotifyGuests
78
+ ? await googleCalendar.getEvent(googleEventId, {
79
+ ownerEmail,
80
+ accountEmail,
81
+ })
82
+ : undefined;
80
83
 
81
- if (args.removeOnly) {
82
- await googleCalendar.removeEventFromCalendar(
83
- googleEventId,
84
- { ownerEmail, accountEmail },
85
- options,
86
- );
87
- } else {
88
- await googleCalendar.deleteEvent(
89
- googleEventId,
90
- { ownerEmail, accountEmail },
91
- options,
92
- );
84
+ if (args.removeOnly) {
85
+ await googleCalendar.removeEventFromCalendar(
86
+ googleEventId,
87
+ { ownerEmail, accountEmail },
88
+ options,
89
+ );
90
+ } else {
91
+ await googleCalendar.deleteEvent(
92
+ googleEventId,
93
+ { ownerEmail, accountEmail },
94
+ options,
95
+ );
96
+ }
97
+ } catch (error) {
98
+ if (!isGoogleNotFoundError(error)) throw error;
99
+
100
+ return {
101
+ success: true,
102
+ alreadyAbsent: true,
103
+ id: `google-${googleEventId}`,
104
+ accountEmail,
105
+ scope: args.scope,
106
+ removedOnly: args.removeOnly ?? false,
107
+ };
93
108
  }
94
109
 
95
110
  const guestNotification =
@@ -105,6 +120,7 @@ export default defineAction({
105
120
 
106
121
  return {
107
122
  success: true,
123
+ alreadyAbsent: false,
108
124
  id: `google-${googleEventId}`,
109
125
  accountEmail,
110
126
  scope: args.scope,
@@ -10,6 +10,7 @@ import {
10
10
  type WeekdayName,
11
11
  } from "../server/lib/event-weekday.js";
12
12
  import { zonedDateTimeToUtcIso } from "../server/lib/find-time.js";
13
+ import { isGoogleNotFoundError } from "../server/lib/google-api.js";
13
14
  import * as googleCalendar from "../server/lib/google-calendar.js";
14
15
  import type { CalendarEvent } from "../shared/api.js";
15
16
  import {
@@ -36,7 +37,7 @@ const MAX_MATCHED_EVENTS = 200;
36
37
  * firing every delete at once and turning a clean batch into retries. */
37
38
  const DELETE_CONCURRENCY = 4;
38
39
 
39
- type Outcome = "deleted" | "matched" | "skipped" | "failed";
40
+ type Outcome = "deleted" | "already_absent" | "matched" | "skipped" | "failed";
40
41
 
41
42
  interface EventResult {
42
43
  id: string;
@@ -449,6 +450,7 @@ export default defineAction({
449
450
  ...summaryBase,
450
451
  dryRun: true,
451
452
  deleted: 0,
453
+ alreadyAbsent: 0,
452
454
  failed: 0,
453
455
  skipped: results.length,
454
456
  events: [...targets.map((target) => target.display), ...results],
@@ -483,6 +485,13 @@ export default defineAction({
483
485
  }
484
486
  return { ...target.display, outcome: "deleted" };
485
487
  } catch (error) {
488
+ if (isGoogleNotFoundError(error)) {
489
+ return {
490
+ ...target.display,
491
+ outcome: "already_absent",
492
+ reason: "Already absent from Google Calendar",
493
+ };
494
+ }
486
495
  return {
487
496
  ...target.display,
488
497
  outcome: "failed",
@@ -498,6 +507,9 @@ export default defineAction({
498
507
  dryRun: false,
499
508
  deleted: deleteResults.filter((entry) => entry.outcome === "deleted")
500
509
  .length,
510
+ alreadyAbsent: deleteResults.filter(
511
+ (entry) => entry.outcome === "already_absent",
512
+ ).length,
501
513
  failed: deleteResults.filter((entry) => entry.outcome === "failed")
502
514
  .length,
503
515
  skipped: results.length,
@@ -35,6 +35,7 @@ import {
35
35
  } from "@/components/ui/tooltip";
36
36
  import { useUpdateEvent } from "@/hooks/use-events";
37
37
  import { useViewPreferences } from "@/hooks/use-view-preferences";
38
+ import { getDisplayDateInTimezone } from "@/lib/calendar-timezone";
38
39
  import { getEditableEventTitle } from "@/lib/event-form-utils";
39
40
  import { isOutOfOfficeEvent } from "@/lib/out-of-office";
40
41
  import { cn } from "@/lib/utils";
@@ -447,15 +448,33 @@ export function EventDetailPanel({
447
448
  ) : (
448
449
  <>
449
450
  <span className="text-foreground">
450
- {format(parseISO(event.start), "h:mm a")}
451
+ {format(
452
+ getDisplayDateInTimezone(
453
+ event.start,
454
+ event.startTimeZone ?? event.endTimeZone,
455
+ ),
456
+ "h:mm a",
457
+ )}
451
458
  {" → "}
452
- {format(parseISO(event.end), "h:mm a")}
459
+ {format(
460
+ getDisplayDateInTimezone(
461
+ event.end,
462
+ event.endTimeZone ?? event.startTimeZone,
463
+ ),
464
+ "h:mm a",
465
+ )}
453
466
  </span>
454
467
  <span className="ml-2 text-muted-foreground/70">
455
468
  {formatDuration(event.start, event.end)}
456
469
  </span>
457
470
  <div className="mt-0.5 text-muted-foreground">
458
- {format(parseISO(event.start), "EEE MMM d")}
471
+ {format(
472
+ getDisplayDateInTimezone(
473
+ event.start,
474
+ event.startTimeZone ?? event.endTimeZone,
475
+ ),
476
+ "EEE MMM d",
477
+ )}
459
478
  </div>
460
479
  </>
461
480
  )}
@@ -118,6 +118,25 @@ export function getDateTimePartsInTimezone(
118
118
  return dateTimeParts(value, normalizeTimezone(timezone));
119
119
  }
120
120
 
121
+ /**
122
+ * Build a local Date carrying an event's wall-clock fields so date-fns can
123
+ * format those fields without converting them through the browser timezone.
124
+ * The result is for display only; it must not be used for date arithmetic.
125
+ */
126
+ export function getDisplayDateInTimezone(
127
+ value: Date | string,
128
+ timezone?: string | null,
129
+ ): Date {
130
+ const parsed = value instanceof Date ? value : new Date(value);
131
+ if (!timezone) return parsed;
132
+
133
+ const parts = getDateTimePartsInTimezone(value, timezone);
134
+ if (!parts) return parsed;
135
+
136
+ const [year, month, day] = parts.date.split("-").map(Number);
137
+ return new Date(year, month - 1, day, parts.hour, parts.minute, parts.second);
138
+ }
139
+
121
140
  export function getDateKeyInTimezone(
122
141
  value: Date | string,
123
142
  timezone: string,
@@ -113,6 +113,23 @@ export function createOAuth2Client(
113
113
  // Authenticated fetch helper
114
114
  // ---------------------------------------------------------------------------
115
115
 
116
+ export class GoogleApiError extends Error {
117
+ readonly status: number;
118
+
119
+ constructor(status: number, message: string) {
120
+ super(`Google API error (${status}): ${message}`);
121
+ this.name = "GoogleApiError";
122
+ this.status = status;
123
+ }
124
+ }
125
+
126
+ export function isGoogleNotFoundError(error: unknown): boolean {
127
+ return (
128
+ (error instanceof GoogleApiError && error.status === 404) ||
129
+ (error instanceof Error && /^Google API error \(404\):/.test(error.message))
130
+ );
131
+ }
132
+
116
133
  export async function googleFetch(
117
134
  url: string,
118
135
  accessToken: string,
@@ -133,7 +150,7 @@ export async function googleFetch(
133
150
  (data as any)?.error?.message ||
134
151
  (data as any)?.error_description ||
135
152
  res.statusText;
136
- throw new Error(`Google API error (${res.status}): ${msg}`);
153
+ throw new GoogleApiError(res.status, msg);
137
154
  }
138
155
 
139
156
  return data;
@@ -27,6 +27,17 @@ import { loadAgentsMdContext } from "./lib/agents-md-context.js";
27
27
  // handles crashed processes without stealing an active text-model run.
28
28
  const PENDING_STALE_MS = 2 * 60 * 1000;
29
29
 
30
+ export function skippedFinalizeResult(meetingId: string) {
31
+ return {
32
+ meetingId,
33
+ summaryMd: "",
34
+ bullets: [],
35
+ actionItems: [],
36
+ provider: null,
37
+ skipped: "no-transcript" as const,
38
+ };
39
+ }
40
+
30
41
  function trimContextValue(value: string, maxChars: number): string {
31
42
  const trimmed = value.replace(/\0/g, "").trim();
32
43
  if (trimmed.length <= maxChars) return trimmed;
@@ -80,6 +91,11 @@ export default defineAction({
80
91
  updatedAt: nowIso,
81
92
  })
82
93
  .where(eq(schema.meetings.id, args.meetingId));
94
+ // The desktop stop path can finish before its final transcript flush
95
+ // lands. That is a completed recording without notes, not an action
96
+ // failure worth returning as HTTP 500; the explicit regenerate flow
97
+ // still receives the actionable error below.
98
+ if (!args.force) return skippedFinalizeResult(args.meetingId);
83
99
  throw new Error(
84
100
  `Cannot finalize meeting ${args.meetingId} — no transcript text available yet.`,
85
101
  );
@@ -372,6 +372,7 @@ export default defineAction({
372
372
  await recordCalendarFetchError(
373
373
  account,
374
374
  new Error("Token refresh failed"),
375
+ { needsReauth: true },
375
376
  ),
376
377
  );
377
378
  continue;
@@ -23,6 +23,8 @@ import { z } from "zod";
23
23
 
24
24
  import { getDb, schema } from "../server/db/index.js";
25
25
  import {
26
+ recordCalendarFetchError,
27
+ recordCalendarFetchSuccess,
26
28
  resolveCalendarAccessToken,
27
29
  shouldMarkNeedsReauth,
28
30
  } from "../server/lib/calendar-event-meetings.js";
@@ -98,24 +100,11 @@ export default defineAction({
98
100
  // sync error via shouldMarkNeedsReauth without flipping status.
99
101
  const accessToken = await resolveCalendarAccessToken(account);
100
102
  if (!accessToken) {
101
- // Fix 10: isolate this account's needs-reauth write in its own
102
- // try/catch so a downstream throw on a different account doesn't
103
- // leave the flag unpersisted.
104
- try {
105
- await db
106
- .update(schema.calendarAccounts)
107
- .set({
108
- status: "needs-reauth",
109
- lastSyncError: "Token refresh failed — reconnect required.",
110
- updatedAt: new Date().toISOString(),
111
- })
112
- .where(eq(schema.calendarAccounts.id, account.id));
113
- } catch (writeErr: any) {
114
- console.warn(
115
- `[sync-calendars] failed to flag account ${account.id} as needs-reauth:`,
116
- writeErr?.message ?? writeErr,
117
- );
118
- }
103
+ await recordCalendarFetchError(
104
+ account,
105
+ new Error("Token refresh failed"),
106
+ { needsReauth: true },
107
+ );
119
108
  errors.push({
120
109
  accountId: account.id,
121
110
  error: "needs-reauth",
@@ -206,18 +195,10 @@ export default defineAction({
206
195
  perAccountEvents += 1;
207
196
  }
208
197
 
209
- // Fix 10: isolate the success-path status write so other accounts'
210
- // needs-reauth flags can't be wiped by a later account-level throw.
198
+ // Keep the success write isolated and guarded so an in-flight sweep
199
+ // cannot overwrite a concurrent needs-reauth decision.
211
200
  try {
212
- await db
213
- .update(schema.calendarAccounts)
214
- .set({
215
- lastSyncedAt: new Date().toISOString(),
216
- lastSyncError: null,
217
- status: "connected",
218
- updatedAt: new Date().toISOString(),
219
- })
220
- .where(eq(schema.calendarAccounts.id, account.id));
201
+ await recordCalendarFetchSuccess(account);
221
202
  } catch (writeErr: any) {
222
203
  console.warn(
223
204
  `[sync-calendars] failed to update account ${account.id} after success:`,
@@ -4,6 +4,7 @@ import { open as openExternal } from "@tauri-apps/plugin-shell";
4
4
  import { useCallback, useEffect, useMemo, useRef } from "react";
5
5
 
6
6
  import { callAppBundleIdsForJoinUrl } from "../lib/meeting-call-app";
7
+ import { stopMeetingBeforeTranscriptFlush } from "../lib/meeting-stop";
7
8
  import {
8
9
  appendFinalTranscript,
9
10
  onFinalTranscript,
@@ -195,18 +196,33 @@ export function useMeetingTranscription({
195
196
  }
196
197
  });
197
198
  await invoke("silence_detector_stop").catch(() => {});
198
- if (reason !== "app-quit") {
199
- await session.historyInFlight?.catch(() => {});
200
- }
201
- // Final flush waits for any in-flight flush first (flushTranscript's
202
- // single-flight coalescing) then sends the definitive snapshot.
203
- await flushTranscript().catch((err) => {
204
- console.warn("[clips-popover] meeting transcript save failed:", err);
205
- });
206
- await callClipsAction("stop-meeting-recording", {
207
- meetingId: session.meetingId,
208
- }).catch((err) => {
209
- console.warn("[clips-popover] stop meeting action failed:", err);
199
+ await stopMeetingBeforeTranscriptFlush({
200
+ // Stamp actualEnd as soon as capture is torn down. Transcript
201
+ // history and network flushes may be slow or unavailable, but they
202
+ // must not leave the meeting looking live in the meantime.
203
+ stopRecording: async () => {
204
+ await callClipsAction("stop-meeting-recording", {
205
+ meetingId: session.meetingId,
206
+ }).catch((err) => {
207
+ console.warn("[clips-popover] stop meeting action failed:", err);
208
+ });
209
+ },
210
+ waitForHistory: async () => {
211
+ if (reason !== "app-quit") {
212
+ await session.historyInFlight?.catch(() => {});
213
+ }
214
+ },
215
+ // Final flush waits for any in-flight flush first
216
+ // (flushTranscript's single-flight coalescing) and sends the
217
+ // definitive snapshot.
218
+ flushTranscript: async () => {
219
+ await flushTranscript().catch((err) => {
220
+ console.warn(
221
+ "[clips-popover] meeting transcript save failed:",
222
+ err,
223
+ );
224
+ });
225
+ },
210
226
  });
211
227
  if (session.lines.length) {
212
228
  const finalizePromise = callClipsAction("finalize-meeting", {
@@ -0,0 +1,16 @@
1
+ interface StopMeetingBeforeTranscriptFlushOptions {
2
+ stopRecording: () => Promise<void>;
3
+ waitForHistory: () => Promise<void>;
4
+ flushTranscript: () => Promise<void>;
5
+ }
6
+
7
+ /** Persist the terminal meeting state before waiting on transcript work. */
8
+ export async function stopMeetingBeforeTranscriptFlush({
9
+ stopRecording,
10
+ waitForHistory,
11
+ flushTranscript,
12
+ }: StopMeetingBeforeTranscriptFlushOptions): Promise<void> {
13
+ await stopRecording();
14
+ await waitForHistory();
15
+ await flushTranscript();
16
+ }
@@ -23,6 +23,8 @@ import {
23
23
 
24
24
  export const CALENDAR_MEETING_ID_PREFIX = "gcal";
25
25
 
26
+ const CALENDAR_REQUEST_SAFETY_MARGIN_MS = 60 * 1000;
27
+
26
28
  export interface CalendarAccountForEvents {
27
29
  id: string;
28
30
  provider: string;
@@ -74,8 +76,7 @@ export function shouldMarkNeedsReauth(message: string): boolean {
74
76
  lower.includes("google calendar event failed (401)") ||
75
77
  lower.includes("invalid_grant") ||
76
78
  lower.includes("invalid_token") ||
77
- lower.includes("insufficient_scope") ||
78
- lower.includes("token refresh failed")
79
+ lower.includes("insufficient_scope")
79
80
  );
80
81
  }
81
82
 
@@ -122,10 +123,16 @@ export async function resolveCalendarAccessToken(
122
123
  });
123
124
  } catch (err) {
124
125
  // Only a permanent failure (dead refresh token / bad OAuth client) means
125
- // "needs-reauth" — collapse those to `null` as before. A transient
126
- // failure (network error, 429, 5xx, timeout) is rethrown so callers can
127
- // record it as a soft sync error without flipping account status.
126
+ // "needs-reauth" — collapse those to `null` as before. During the refresh
127
+ // buffer, a transient failure can safely reuse the still-valid token.
128
128
  if (isPermanentRefreshFailure(err)) return null;
129
+ if (
130
+ bundle?.accessToken &&
131
+ bundle.expiresAt &&
132
+ bundle.expiresAt > Date.now() + CALENDAR_REQUEST_SAFETY_MARGIN_MS
133
+ ) {
134
+ return bundle.accessToken;
135
+ }
129
136
  throw err;
130
137
  }
131
138
  if (!refreshed.access_token) return null;
@@ -158,13 +165,13 @@ export async function recordCalendarFetchSuccess(
158
165
  .set({
159
166
  lastSyncedAt: now,
160
167
  lastSyncError: null,
161
- status: "connected",
162
168
  updatedAt: now,
163
169
  })
164
170
  .where(
165
171
  and(
166
172
  eq(schema.calendarAccounts.id, account.id),
167
173
  eq(schema.calendarAccounts.ownerEmail, account.ownerEmail),
174
+ eq(schema.calendarAccounts.status, "connected"),
168
175
  ),
169
176
  );
170
177
  }
@@ -172,10 +179,11 @@ export async function recordCalendarFetchSuccess(
172
179
  export async function recordCalendarFetchError(
173
180
  account: CalendarAccountForEvents,
174
181
  error: unknown,
182
+ options: { needsReauth?: boolean } = {},
175
183
  ): Promise<CalendarFetchError> {
176
184
  const message =
177
185
  error instanceof Error ? error.message : String(error || "Calendar failed");
178
- const needsReauth = shouldMarkNeedsReauth(message);
186
+ const needsReauth = options.needsReauth ?? shouldMarkNeedsReauth(message);
179
187
  if (!account.ownerEmail) {
180
188
  return { accountId: account.id, error: message, needsReauth };
181
189
  }
@@ -183,7 +191,7 @@ export async function recordCalendarFetchError(
183
191
  await db
184
192
  .update(schema.calendarAccounts)
185
193
  .set({
186
- status: needsReauth ? "needs-reauth" : "connected",
194
+ ...(needsReauth ? { status: "needs-reauth" as const } : {}),
187
195
  lastSyncError: needsReauth
188
196
  ? "Google Calendar needs to be reconnected."
189
197
  : message,
@@ -320,7 +328,9 @@ export async function fetchLiveCalendarEventFromId(virtualId: string) {
320
328
  return null;
321
329
  }
322
330
  if (!accessToken) {
323
- await recordCalendarFetchError(account, new Error("Token refresh failed"));
331
+ await recordCalendarFetchError(account, new Error("Token refresh failed"), {
332
+ needsReauth: true,
333
+ });
324
334
  return null;
325
335
  }
326
336
 
@@ -1042,6 +1042,7 @@ export function SlashCommandMenu({
1042
1042
 
1043
1043
  const handleKeyDown = (e: KeyboardEvent) => {
1044
1044
  if (
1045
+ !isOpen &&
1045
1046
  e.key === "Enter" &&
1046
1047
  !e.shiftKey &&
1047
1048
  !e.metaKey &&
@@ -729,6 +729,11 @@
729
729
  background: hsl(var(--muted));
730
730
  border-radius: 0 0 6px 6px;
731
731
  }
732
+ .notion-editor .notion-code-block {
733
+ background: hsl(var(--muted));
734
+ border-radius: 6px;
735
+ margin: 0.6em 0;
736
+ }
732
737
  .notion-editor pre code {
733
738
  font-family: inherit;
734
739
  background: none;