@agent-native/core 0.168.7 → 0.168.9

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 (84) 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/factory/.agents/skills/factory-graphs/SKILL.md +5 -0
  15. package/corpus/templates/factory/AGENTS.md +12 -22
  16. package/corpus/templates/factory/README.md +11 -8
  17. package/corpus/templates/factory/actions/approve-factory-item.ts +35 -8
  18. package/corpus/templates/factory/actions/babysit-agent-native-pull-request.ts +24 -9
  19. package/corpus/templates/factory/actions/create-factory.ts +211 -0
  20. package/corpus/templates/factory/actions/evaluate-triage-item.ts +29 -5
  21. package/corpus/templates/factory/actions/get-factory-graph.ts +1 -1
  22. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +20 -11
  23. package/corpus/templates/factory/actions/get-triage-config.ts +13 -12
  24. package/corpus/templates/factory/actions/get-triage-item.ts +28 -6
  25. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +33 -17
  26. package/corpus/templates/factory/actions/ingest-github-observation.ts +10 -4
  27. package/corpus/templates/factory/actions/list-factory-audit.ts +11 -4
  28. package/corpus/templates/factory/actions/list-factory-automations.ts +34 -12
  29. package/corpus/templates/factory/actions/list-triage-items.ts +11 -3
  30. package/corpus/templates/factory/actions/list-triage-rules.ts +11 -4
  31. package/corpus/templates/factory/actions/poll-github-sources.ts +24 -10
  32. package/corpus/templates/factory/actions/poll-sentry-errors.ts +26 -11
  33. package/corpus/templates/factory/actions/poll-slack-channel.ts +31 -13
  34. package/corpus/templates/factory/actions/reconcile-triage-run.ts +59 -9
  35. package/corpus/templates/factory/actions/record-triage-feedback.ts +14 -3
  36. package/corpus/templates/factory/actions/run-factory-automation.ts +12 -2
  37. package/corpus/templates/factory/actions/save-factory-automation.ts +82 -21
  38. package/corpus/templates/factory/actions/save-factory-graph.ts +10 -1
  39. package/corpus/templates/factory/actions/save-triage-config.ts +90 -36
  40. package/corpus/templates/factory/actions/save-triage-rule.ts +16 -3
  41. package/corpus/templates/factory/actions/start-builder-for-item.ts +28 -20
  42. package/corpus/templates/factory/actions/suggest-factory-rules.ts +11 -4
  43. package/corpus/templates/factory/actions/view-screen.ts +22 -30
  44. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +7 -2
  45. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +7 -22
  46. package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +260 -0
  47. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  48. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +2 -1
  49. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -3
  50. package/corpus/templates/factory/app/i18n/en-US.ts +18 -1
  51. package/corpus/templates/factory/app/routes/factory.tsx +78 -62
  52. package/corpus/templates/factory/app/routes/{factory-settings.tsx → new-factory.tsx} +12 -9
  53. package/corpus/templates/factory/server/db/schema.ts +18 -3
  54. package/corpus/templates/factory/server/factory-graph/contracts.ts +24 -0
  55. package/corpus/templates/factory/server/factory-graph/store.ts +16 -9
  56. package/corpus/templates/factory/server/lib/factory-automation-plan.ts +136 -0
  57. package/corpus/templates/factory/server/lib/factory-automation-repair.ts +31 -0
  58. package/corpus/templates/factory/server/lib/factory-scope.ts +416 -0
  59. package/corpus/templates/factory/server/lib/require-factory-automation.ts +11 -0
  60. package/corpus/templates/factory/server/plugins/agent-chat.ts +4 -2
  61. package/corpus/templates/factory/server/plugins/factory-migrations.ts +80 -0
  62. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +142 -43
  63. package/corpus/templates/factory/server/triage/audit.ts +4 -0
  64. package/corpus/templates/factory/server/triage/ids.ts +4 -0
  65. package/dist/cli/native-dependencies.js +4 -1
  66. package/dist/client/AgentPanel.d.ts +5 -1
  67. package/dist/client/AgentPanel.js +9 -4
  68. package/dist/client/agent-chat/index.d.ts +1 -1
  69. package/dist/client/agent-chat/index.js +1 -1
  70. package/dist/client/chat-first/apps-rail.js +6 -6
  71. package/dist/client/chat-first/primary-nav.js +14 -6
  72. package/dist/client/integrations/IntegrationsPanel.d.ts +15 -0
  73. package/dist/client/integrations/IntegrationsPanel.js +29 -21
  74. package/dist/client/integrations/index.d.ts +1 -1
  75. package/dist/client/integrations/index.js +1 -1
  76. package/dist/client/resources/McpIntegrationDialog.js +1 -1
  77. package/dist/collab/struct-routes.d.ts +1 -1
  78. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  79. package/dist/observability/routes.d.ts +3 -3
  80. package/dist/progress/routes.d.ts +1 -1
  81. package/dist/secrets/routes.d.ts +6 -6
  82. package/dist/server/realtime-token.d.ts +1 -1
  83. package/dist/server/transcribe-voice.d.ts +1 -1
  84. 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: 8502
@@ -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
 
@@ -16,6 +16,11 @@ explains them, but never hides a topology change in prose. Until an execution
16
16
  binding is explicitly added, the blueprint must not be described as the runtime
17
17
  router.
18
18
 
19
+ Runtime work — settings, inbox, rules, automations, and activity — is scoped by
20
+ `factoryId`. Reusable agents stay workspace-wide; graph nodes may reference any
21
+ workspace agent as blueprint only. Pass `factoryId` on triage, config, automation,
22
+ and audit actions for the factory the user is viewing.
23
+
19
24
  ## Workflow
20
25
 
21
26
  1. Read `view-screen` to identify the selected Factory, tab, node, or edge.
@@ -21,22 +21,11 @@ decisions, feedback, agent runs, and provider audit records.
21
21
  reconciliation is not success; preserve typed failure or
22
22
  `reconciliation_required` state.
23
23
  - Deduplicate by Factory item and rule/run identity, not provider comment ID.
24
- - Slack clear bugs go through `start-builder-for-item` (👀 and a Slack user
25
- mention for `/address-feedback`); never post Slack messages or `@handles`.
26
- Group repeats into one Builder thread. GitHub/Sentry use the Builder run
27
- API. Follow `review-latest-feedback` for full-thread evidence, existing
28
- reaction and owner checks, answered-clarification priority, one-question
29
- clarification limits, durable rechecks, and the required `@agent-native`
30
- disposition after every actionable 👀. Clips, Design, and Content stay
31
- owner-managed outside autonomous dispatch and PR governance.
32
- - PR governance follows `review-prs`: verify BuilderIO membership and complete
33
- diff/review/check evidence; skip drafts/current approvals and never approve
34
- external authors. Keep ultra-scary security, auth, tenant-isolation,
35
- secrets, data-loss, execution, payment, and deployment risks manual.
36
- Verified members may pass ordinary uncertain checks or review feedback, but
37
- record exact states and never call them clean. Apply the Alice/Content,
38
- Nick/Slides, Enzo/Factory, Sid/Design, and docs-only exceptions only after
39
- membership and the ultra-scary gate. Never auto-merge.
24
+ - Slack clear bugs go through `start-builder-for-item`; never post Slack
25
+ messages or `@handles`. GitHub/Sentry use the Builder run API. Read
26
+ `review-latest-feedback` for thread evidence and disposition rules.
27
+ - PR governance follows `review-prs`: verify membership and evidence; skip
28
+ drafts and external authors; keep ultra-scary risks manual; never auto-merge.
40
29
  - Graph edits create immutable blueprint versions. AI proposes with `source=ai`;
41
30
  a person reviews and publishes through the same action surface.
42
31
  - Provider credentials belong to Dispatch/shared workspace integrations, never
@@ -46,16 +35,16 @@ decisions, feedback, agent runs, and provider audit records.
46
35
 
47
36
  ## Application state
48
37
 
49
- - `navigation.view` is `factory`, `agents`, or `factory-settings`.
50
- Observation settings are workspace-scoped at `/factory-settings`.
51
- `factoryId`, `factoryTab`, `factoryAuditRunId`, `factoryNodeId`, and
52
- `factoryEdgeId` hold Factory context. Read `view-screen` first.
38
+ - `navigation.view` is `factory` or `agents`. Runtime data is scoped by
39
+ `factoryId`; reusable agents stay workspace-wide. Read `view-screen` and
40
+ `factory-graphs` for tab and selection keys.
53
41
 
54
42
  ## Action contract
55
43
 
56
44
  | Action | Purpose |
57
45
  | --- | --- |
58
- | `list-triage-items` / `get-triage-item` | Inspect bounded queue evidence; scheduled reviewers pass `needsReview: true`, `source`, and `limit`. |
46
+ | `list-triage-items` / `get-triage-item` | Inspect queue evidence; pass `factoryId`. |
47
+ | `get-triage-config` / `save-triage-config` | Read or save observation settings for one factory. |
59
48
  | `poll-slack-channel` | Observe Slack history; never writes to Slack. |
60
49
  | `get-slack-feedback-context` | Read the bounded full Slack thread before classification. |
61
50
  | `poll-github-sources` / `poll-sentry-errors` | Observe bounded source queues. |
@@ -67,11 +56,12 @@ decisions, feedback, agent runs, and provider audit records.
67
56
  | `start-builder-for-item` | Govern clear-bug dispatch through Slack or Builder API, or record a skip reason. |
68
57
  | `govern-agent-native-pull-request` | Apply PR evidence and ownership gates. |
69
58
  | `list-factory-automations` / `save-factory-automation` / `run-factory-automation` | Inspect or edit org-owned automations. |
70
- | `list-factory-audit` | Inspect automation runs, evidence, decisions, and provider actions. |
59
+ | `list-factory-audit` | Inspect runs, evidence, and provider actions for one factory. |
71
60
  | `get-factory-automation-health` | Inspect scheduler heartbeat and last error. |
72
61
  | `suggest-factory-rules` | Mine feedback into proposals. |
73
62
  | `reconcile-triage-run` | Persist callback/provider reconciliation. |
74
63
  | `list-factories` / `get-factory-graph` | Inspect definitions, versions, and metrics. |
64
+ | `create-factory` | Create a factory from `/new-factory` with optional sources. |
75
65
  | `save-factory-graph` | Create/version a graph with inspected `expectedGraphVersion`; never starts provider work. |
76
66
  | graph history actions | Factory graph version history. |
77
67
  | `list-factory-comments` / `add-factory-comment` | Read or attach comments to a canvas, node, or edge. |
@@ -44,19 +44,22 @@ shared `app_secrets` rows must use the same
44
44
  encryption fallback). Never copy raw tokens between apps or add a second
45
45
  env-only read in a provider client.
46
46
 
47
- Factory's default observer still keeps organization-scoped source metadata -
48
- such as a Slack channel, repository, or Sentry project - for its normalized
49
- queue adapters. That metadata is not a credential or a per-factory integration
50
- setup. GitHub issue polling defaults to hourly; Slack and pull-request checks
51
- use shorter bounded cadences because those sources can need faster feedback.
52
- Every poll preserves errors for reconciliation.
47
+ Factory's observer keeps **per-factory** source metadata — Slack channel,
48
+ repository, Sentry project, and related polling settings for its normalized
49
+ queue adapters. Each factory has its own inbox, rules, automations, and activity.
50
+ Reusable agents and workspace credentials stay shared. Each factory's observation
51
+ settings live on its Settings tab in the factory detail view. That metadata is
52
+ not a credential or a per-factory
53
+ integration setup. GitHub issue polling defaults to hourly; Slack and
54
+ pull-request checks use shorter bounded cadences because those sources can need
55
+ faster feedback. Every poll preserves errors for reconciliation.
53
56
 
54
57
  Factory agents can discover connected provider APIs with
55
58
  `provider-api-catalog`, inspect their docs with `provider-api-docs`, and call
56
59
  them through `provider-api-request`. Scheduled Factory runs also receive the
57
60
  workspace's connected MCP tools, subject to the same workspace and request
58
- scope gates. The three normalized pollers are compatibility adapters for the
59
- default triage queue, not the agent's capability limit.
61
+ scope gates. The three normalized pollers are compatibility adapters scoped by
62
+ `factoryId`, not the agent's capability limit.
60
63
 
61
64
  The generic Slack bot is wired to Factory. Mention `@agent-native` in a feedback
62
65
  thread to inspect the linked item, explain its decision, tune a rule, or say