@vellumai/assistant 0.10.6-staging.1 → 0.10.6

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 (58) hide show
  1. package/openapi.yaml +43 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
  4. package/src/__tests__/attachments-store.test.ts +37 -1
  5. package/src/__tests__/attachments.test.ts +53 -0
  6. package/src/__tests__/channel-reply-delivery.test.ts +214 -5
  7. package/src/__tests__/config-loader-backfill.test.ts +16 -36
  8. package/src/__tests__/image-source-path-reinject.test.ts +155 -14
  9. package/src/__tests__/managed-profile-guard.test.ts +2 -0
  10. package/src/__tests__/mtime-cache.test.ts +265 -80
  11. package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
  12. package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
  13. package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
  14. package/src/agent/attachments.ts +55 -13
  15. package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
  16. package/src/cli/commands/platform/index.ts +86 -4
  17. package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
  18. package/src/cli/lib/diff-plugin.ts +7 -3
  19. package/src/cli/lib/inspect-plugin.ts +28 -10
  20. package/src/cli/lib/install-from-github.ts +69 -24
  21. package/src/cli/lib/ipc-params.ts +3 -3
  22. package/src/cli/lib/list-installed-plugins.ts +11 -5
  23. package/src/cli/lib/merge-plugin-tree.ts +15 -9
  24. package/src/cli/lib/plugin-fingerprint.ts +0 -6
  25. package/src/cli/lib/publish-plugin.ts +1 -1
  26. package/src/cli/lib/unknown-command.ts +3 -1
  27. package/src/cli/lib/upgrade-plugin.ts +2 -5
  28. package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
  29. package/src/config/seed-inference-profiles.ts +5 -4
  30. package/src/daemon/conversation-lifecycle.ts +37 -15
  31. package/src/daemon/conversation-messaging.ts +56 -28
  32. package/src/daemon/conversation.ts +2 -2
  33. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
  34. package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
  35. package/src/hooks/hook-loader.ts +57 -144
  36. package/src/hooks/registry.ts +13 -9
  37. package/src/persistence/attachments-store.ts +2 -1
  38. package/src/persistence/conversation-crud.ts +24 -0
  39. package/src/plugin-api/types.ts +4 -1
  40. package/src/plugins/mtime-cache.ts +347 -33
  41. package/src/plugins/pipeline.ts +3 -2
  42. package/src/providers/__tests__/inference.test.ts +42 -31
  43. package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
  44. package/src/providers/inference/auth.ts +1 -2
  45. package/src/providers/inference/backfill.ts +7 -5
  46. package/src/providers/inference/connections.ts +54 -30
  47. package/src/runtime/channel-reply-delivery.ts +83 -24
  48. package/src/runtime/channel-retry-sweep.ts +4 -0
  49. package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
  50. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
  51. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
  52. package/src/runtime/routes/conversation-query-routes.ts +18 -1
  53. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  54. package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
  55. package/src/runtime/routes/platform-routes.ts +82 -1
  56. package/src/runtime/slack-reply-session.ts +1 -1
  57. package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
  58. package/src/workspace/migrations/registry.ts +2 -0
@@ -4,19 +4,27 @@
4
4
  *
5
5
  * A "hook" is a named lifecycle event (`init`, `shutdown`, `user-prompt-submit`,
6
6
  * `post-tool-use`, `stop`, ...) handled by a default export. Hooks come from
7
- * two surfaces, both cached here keyed by their source file's mtime:
7
+ * two surfaces:
8
8
  *
9
9
  * - **Plugin hooks** — `<workspace>/plugins/<name>/hooks/<event>.{ts,js}`,
10
10
  * discovered alongside the owning plugin's tools.
11
11
  * - **Workspace hooks** — `<workspace>/hooks/<event>.{ts,js}`, standalone
12
12
  * files not tied to any plugin (no `package.json`, no tools — just hooks).
13
13
  *
14
+ * Hook dispatch is a hot path, so collecting hooks costs no filesystem
15
+ * operations: every hook function enters the in-memory cache when its owner
16
+ * is (re)activated ({@link preImportHooksDir}), and dispatch is pure map
17
+ * lookups. Source changes reach the cache through the source-versions
18
+ * reconcile in `../plugins/mtime-cache.ts`, which tears the owner down,
19
+ * evicts its cache entries and modules, and reactivates it — dispatch never
20
+ * stats, lists, or imports.
21
+ *
14
22
  * This module owns the hook cache and every hook operation (collect, init,
15
23
  * shutdown, eviction). Plugin *discovery* (which plugin directories exist, in
16
24
  * what order) lives in `../plugins/mtime-cache.ts`; the orchestrator there
17
25
  * passes the discovered directories into {@link collectUserHooks} and drives
18
- * pre-import / init / shutdown at boot. Keeping discovery out of this module
19
- * lets it sit below the plugin cache with no import cycle.
26
+ * pre-import / init / shutdown. Keeping discovery out of this module lets it
27
+ * sit below the plugin cache with no import cycle.
20
28
  */
21
29
 
22
30
  import {
@@ -37,11 +45,7 @@ import type {
37
45
  ShutdownContext,
38
46
  } from "../plugin-api/types.js";
39
47
  import { listSurfaceDir } from "../plugins/external-plugin-loader.js";
40
- import {
41
- evictModule,
42
- getMtime,
43
- importWithTimeout,
44
- } from "../plugins/surface-import.js";
48
+ import { evictModule, importWithTimeout } from "../plugins/surface-import.js";
45
49
  import type { HookEntry } from "../plugins/types.js";
46
50
  import { getLogger } from "../util/logger.js";
47
51
  import { getWorkspaceDir, getWorkspaceHooksDir } from "../util/platform.js";
@@ -60,20 +64,17 @@ const log = getLogger("hook-loader");
60
64
  */
61
65
  export const WORKSPACE_HOOKS_OWNER = "__workspace__";
62
66
 
63
- /**
64
- * A cached hook function plus the mtime of its source file. When the on-disk
65
- * mtime changes, the hook is re-imported and the entry is replaced.
66
- */
67
+ /** A cached, imported hook function. */
67
68
  interface CachedHook {
68
69
  readonly hook: HookFunction;
69
- /** mtimeMs of the source file this hook was imported from. */
70
- readonly sourceMtime: number;
71
70
  }
72
71
 
73
72
  /**
74
73
  * Cached hooks keyed by `${ownerName}/${hookName}`. The key includes the
75
74
  * owner (plugin name, or {@link WORKSPACE_HOOKS_OWNER}) so hooks from
76
- * different owners don't collide.
75
+ * different owners don't collide. Entries are created when an owner is
76
+ * (re)activated and removed only by owner-level eviction — the cache is the
77
+ * complete record of an owner's dispatchable hooks.
77
78
  */
78
79
  const hookCache = new Map<string, CachedHook>();
79
80
 
@@ -83,87 +84,17 @@ function hookKey(ownerName: string, hookName: string): string {
83
84
  }
84
85
 
85
86
  /**
86
- * Resolve a single hook file through the mtime cache: return the cached hook
87
- * when its source mtime is unchanged, otherwise re-import and refresh the
88
- * entry. Returns `undefined` when the file was deleted (evicting any stale
89
- * entry) or the import failed / produced a non-function default export.
90
- *
91
- * `ownerName` is the cache-key prefix and the attribution label in logs (a
92
- * plugin name, or {@link WORKSPACE_HOOKS_OWNER}).
93
- */
94
- async function resolveCachedHook<TCtx>(
95
- ownerName: string,
96
- hookName: string,
97
- filePath: string,
98
- ): Promise<HookFunction<TCtx> | undefined> {
99
- const key = hookKey(ownerName, hookName);
100
- const currentMtime = getMtime(filePath);
101
-
102
- // Cache hit — same mtime.
103
- const cached = hookCache.get(key);
104
- if (
105
- cached !== undefined &&
106
- cached.sourceMtime === currentMtime &&
107
- currentMtime > 0
108
- ) {
109
- return cached.hook as HookFunction<TCtx>;
110
- }
111
-
112
- // Cache miss — re-import.
113
- if (currentMtime === 0) {
114
- // File was deleted between listing and stat — evict the cache entry.
115
- hookCache.delete(key);
116
- return undefined;
117
- }
118
-
119
- // The file is new, recreated, or edited since it was last imported. Evict
120
- // it from the runtime module registry first: without this, the import
121
- // below would return the module Bun cached at the old content and the
122
- // edit would never take effect. No-op for a first-ever load.
123
- //
124
- // Note the reload swaps only the exported function for future dispatches.
125
- // The hook file's top-level code runs again on re-import, and nothing
126
- // tears down the previous instance — so hook files must keep long-lived
127
- // side effects (timers, listeners) in the owner's `init`/`shutdown`
128
- // lifecycle, not at module top level.
129
- evictModule(filePath);
130
-
131
- try {
132
- const hook = await importWithTimeout<HookFunction>(filePath);
133
- if (hook === undefined || typeof hook !== "function") {
134
- log.error(
135
- { plugin: ownerName, hook: hookName, path: filePath },
136
- `hook ${hookName} default export must be a function (got ${typeof hook}) — skipping`,
137
- );
138
- return undefined;
139
- }
140
- hookCache.set(key, { hook, sourceMtime: currentMtime });
141
- return hook as HookFunction<TCtx>;
142
- } catch (err) {
143
- log.error(
144
- { err, plugin: ownerName, hook: hookName, path: filePath },
145
- `Failed to import hook ${hookName} from ${filePath}`,
146
- );
147
- return undefined;
148
- }
149
- }
150
-
151
- /**
152
- * Collect every hook for a given event name across all surfaces, re-importing
153
- * any whose source file's mtime changed since the cache was populated.
87
+ * Collect every cached hook for a given event name across all surfaces.
88
+ * Pure in-memory lookups — no filesystem operations on the dispatch path.
154
89
  *
155
90
  * `pluginDirs` is the orchestrator's discovered `[dir, ownerName]` set (in
156
91
  * install-date order). Each plugin's hook runs first, then the standalone
157
- * workspace hook under `<workspace>/hooks/<hookName>.{ts,js}` runs last — so
158
- * a plugin can shape the threaded context before a workspace-wide hook
159
- * observes or finalizes it.
92
+ * workspace hook runs last — so a plugin can shape the threaded context
93
+ * before a workspace-wide hook observes or finalizes it.
160
94
  *
161
- * Added, removed, and edited hook files are all picked up live: discovery is
162
- * by directory listing, and a changed source mtime evicts the module from
163
- * the runtime registry before re-import, so the edited hook takes effect on
164
- * the next dispatch without a daemon restart. Eviction is per-file — an edit
165
- * to a helper module a hook imports is not detected (the hook file's own
166
- * mtime is what's watched) and still needs a restart.
95
+ * The cache holds exactly what each owner's last (re)activation imported;
96
+ * added, removed, and edited hook files (including helper modules they
97
+ * import) land when the source-versions reconcile redeploys the owner.
167
98
  *
168
99
  * `effectiveEnabledPlugins` carries the per-chat plugin scope: when non-null, a
169
100
  * plugin whose name is not in the set is skipped (its hooks do not run for this
@@ -177,47 +108,30 @@ export async function collectUserHookEntries<TCtx = unknown>(
177
108
  ): Promise<HookEntry<TCtx>[]> {
178
109
  const out: HookEntry<TCtx>[] = [];
179
110
 
180
- for (const [pluginDir, pluginName] of pluginDirs) {
111
+ for (const [, pluginName] of pluginDirs) {
181
112
  if (
182
113
  effectiveEnabledPlugins != null &&
183
114
  !effectiveEnabledPlugins.has(pluginName)
184
115
  ) {
185
116
  continue;
186
117
  }
187
- const hookFile = listSurfaceDir(join(pluginDir, "hooks")).find(
188
- (f) => f.name === hookName,
189
- );
190
- if (hookFile === undefined) {
191
- continue;
192
- }
193
-
194
- const hook = await resolveCachedHook<TCtx>(
195
- pluginName,
196
- hookName,
197
- hookFile.path,
198
- );
199
- if (hook !== undefined) {
200
- out.push({ fn: hook, owner: { kind: "plugin", id: pluginName } });
118
+ const cached = hookCache.get(hookKey(pluginName, hookName));
119
+ if (cached !== undefined) {
120
+ out.push({
121
+ fn: cached.hook as HookFunction<TCtx>,
122
+ owner: { kind: "plugin", id: pluginName },
123
+ });
201
124
  }
202
125
  }
203
126
 
204
127
  // Standalone workspace hooks: files directly under `<workspace>/hooks/`
205
128
  // that are not part of any plugin (no package.json, no tools — just hooks).
206
- const wsHookFile = listSurfaceDir(getWorkspaceHooksDir()).find(
207
- (f) => f.name === hookName,
208
- );
209
- if (wsHookFile !== undefined) {
210
- const hook = await resolveCachedHook<TCtx>(
211
- WORKSPACE_HOOKS_OWNER,
212
- hookName,
213
- wsHookFile.path,
214
- );
215
- if (hook !== undefined) {
216
- out.push({
217
- fn: hook,
218
- owner: { kind: "workspace", id: WORKSPACE_HOOKS_OWNER },
219
- });
220
- }
129
+ const wsCached = hookCache.get(hookKey(WORKSPACE_HOOKS_OWNER, hookName));
130
+ if (wsCached !== undefined) {
131
+ out.push({
132
+ fn: wsCached.hook as HookFunction<TCtx>,
133
+ owner: { kind: "workspace", id: WORKSPACE_HOOKS_OWNER },
134
+ });
221
135
  }
222
136
 
223
137
  return out;
@@ -242,10 +156,13 @@ export async function collectUserHooks<TCtx = unknown>(
242
156
  }
243
157
 
244
158
  /**
245
- * Pre-import every hook file under `hooksDir` and cache it keyed by
246
- * `${ownerName}/${hookName}`, so the first turn doesn't pay the import cost.
247
- * Best-effort per file: a failing import is logged and skipped. A missing
248
- * directory yields no files (handled by {@link listSurfaceDir}).
159
+ * Import every hook file under `hooksDir` into the cache, keyed by
160
+ * `${ownerName}/${hookName}`. This is the single point where hook code
161
+ * enters the process: it runs at owner (re)activation, and what it caches
162
+ * is exactly what dispatch serves until the owner is next redeployed.
163
+ * Best-effort per file: a failing or non-function import is logged and
164
+ * skipped (the owner's other hooks still load). A missing directory yields
165
+ * no files (handled by {@link listSurfaceDir}).
249
166
  */
250
167
  export async function preImportHooksDir(
251
168
  hooksDir: string,
@@ -253,21 +170,23 @@ export async function preImportHooksDir(
253
170
  ): Promise<void> {
254
171
  for (const file of listSurfaceDir(hooksDir)) {
255
172
  const key = hookKey(ownerName, file.name);
256
- const currentMtime = getMtime(file.path);
257
- if (currentMtime === 0) {
258
- continue;
259
- }
260
173
 
261
- // A plugin directory can be removed and reinstalled while the daemon
262
- // runs; evict any module cached from the prior install so the
263
- // pre-import reflects what's on disk now. No-op at boot.
174
+ // The same path may hold different content than when it was last
175
+ // imported (edit, reinstall at the same path); evict so the import
176
+ // below re-evaluates from disk instead of serving Bun's cached module.
177
+ // No-op for a first-ever load.
264
178
  evictModule(file.path);
265
179
 
266
180
  try {
267
181
  const hook = await importWithTimeout<HookFunction>(file.path);
268
- if (hook !== undefined && typeof hook === "function") {
269
- hookCache.set(key, { hook, sourceMtime: currentMtime });
182
+ if (hook === undefined || typeof hook !== "function") {
183
+ log.error(
184
+ { plugin: ownerName, hook: file.name, path: file.path },
185
+ `hook ${file.name} default export must be a function (got ${typeof hook}) — skipping`,
186
+ );
187
+ continue;
270
188
  }
189
+ hookCache.set(key, { hook });
271
190
  } catch (err) {
272
191
  log.error(
273
192
  { err, plugin: ownerName, hook: file.name, path: file.path },
@@ -527,11 +446,8 @@ export function resetHookCacheForTests(): void {
527
446
  hookCache.clear();
528
447
  }
529
448
 
530
- /** Test-only: inspect the hook cache. */
531
- export function _inspectHookCacheForTests(): Array<{
532
- key: string;
533
- sourceMtime: number;
534
- }> {
449
+ /** Test-only: inspect the hook cache's keys. */
450
+ export function _inspectHookCacheForTests(): string[] {
535
451
  const isTest =
536
452
  process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
537
453
  if (!isTest) {
@@ -539,8 +455,5 @@ export function _inspectHookCacheForTests(): Array<{
539
455
  "_inspectHookCacheForTests may only be called in test environments",
540
456
  );
541
457
  }
542
- return Array.from(hookCache.entries()).map(([key, c]) => ({
543
- key,
544
- sourceMtime: c.sourceMtime,
545
- }));
458
+ return Array.from(hookCache.keys());
546
459
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * {@link getHooksFor} combines both sources: in-process hooks from this
14
14
  * registry (filtered by `isPluginDisabled` at read time) and user-land hooks
15
- * from the mtime cache. The read-time filtering is what makes `assistant
15
+ * from the plugin cache. The read-time filtering is what makes `assistant
16
16
  * plugins disable default-*` take effect immediately in a running assistant
17
17
  * — the hooks stay registered but are filtered out on the next turn.
18
18
  */
@@ -45,7 +45,9 @@ export function registerPluginHooks(
45
45
  hooks: Record<string, HookFunction>,
46
46
  ): void {
47
47
  for (const [hookName, fn] of Object.entries(hooks)) {
48
- if (typeof fn !== "function") continue;
48
+ if (typeof fn !== "function") {
49
+ continue;
50
+ }
49
51
  let list = hookRegistry.get(hookName);
50
52
  if (!list) {
51
53
  list = [];
@@ -81,8 +83,8 @@ export function unregisterPluginHooks(pluginName: string): void {
81
83
  *
82
84
  * In-process default plugin hooks are read from this registry (synchronous)
83
85
  * and filtered by the `.disabled` sentinel at read time via
84
- * {@link isPluginDisabled}. User-land hooks are pulled from the mtime cache
85
- * (async, may re-import). Default hooks are prepended so they compose
86
+ * {@link isPluginDisabled}. User-land hooks are pulled from the plugin cache
87
+ * (async; applies any pending source-versions reconcile first). Default hooks are prepended so they compose
86
88
  * innermost, ahead of any user plugins.
87
89
  *
88
90
  * The `TCtx` generic mirrors {@link HookFunction}'s — callers parameterize
@@ -108,9 +110,8 @@ export async function getHookEntriesFor<TCtx = unknown>(
108
110
  // hook-dispatch, well after boot). The module is cached after the first load.
109
111
  let effectiveEnabledPlugins: Set<string> | null = null;
110
112
  if (options?.conversationId) {
111
- const { resolveConversationPluginScope } = await import(
112
- "../daemon/conversation-plugin-scope.js"
113
- );
113
+ const { resolveConversationPluginScope } =
114
+ await import("../daemon/conversation-plugin-scope.js");
114
115
  effectiveEnabledPlugins = resolveConversationPluginScope(
115
116
  options.conversationId,
116
117
  );
@@ -121,7 +122,9 @@ export async function getHookEntriesFor<TCtx = unknown>(
121
122
  // registered but are filtered out on the next turn.
122
123
  const defaultEntries: HookEntry<TCtx>[] = [];
123
124
  for (const entry of hookRegistry.get(name) ?? []) {
124
- if (isPluginDisabled(entry.pluginName)) continue;
125
+ if (isPluginDisabled(entry.pluginName)) {
126
+ continue;
127
+ }
125
128
  if (
126
129
  effectiveEnabledPlugins != null &&
127
130
  !effectiveEnabledPlugins.has(entry.pluginName)
@@ -134,7 +137,8 @@ export async function getHookEntriesFor<TCtx = unknown>(
134
137
  });
135
138
  }
136
139
 
137
- // User-land hooks from the mtime cache (async, may re-import). The per-chat
140
+ // User-land hooks from the plugin cache (async; applies any pending
141
+ // source-versions reconcile first). The per-chat
138
142
  // scope is threaded through so a deselected user plugin's hooks are excluded
139
143
  // too — standalone workspace hooks (not owned by a plugin) always run.
140
144
  const userEntries = await getUserHookEntriesFor<TCtx>(
@@ -827,7 +827,7 @@ export function attachInlineAttachmentToMessage(
827
827
  */
828
828
  normalizeImage?: boolean;
829
829
  },
830
- ): StoredAttachment {
830
+ ): StoredAttachment & { filePath: string } {
831
831
  if (options?.normalizeImage) {
832
832
  ({ filename, mimeType, dataBase64 } = normalizeUploadedImageBase64(
833
833
  filename,
@@ -891,6 +891,7 @@ export function attachInlineAttachmentToMessage(
891
891
  kind,
892
892
  thumbnailBase64: null,
893
893
  createdAt: now,
894
+ filePath: targetPath,
894
895
  };
895
896
  }
896
897
 
@@ -206,6 +206,13 @@ export const messageMetadataSchema = z
206
206
  forkSourceMessageId: z.string().optional(),
207
207
  /** Image source paths from desktop attachments, keyed by filename. */
208
208
  imageSourcePaths: z.record(z.string(), z.string()).optional(),
209
+ /**
210
+ * Resolved paths of the canonical attachment copies in the conversation's
211
+ * attachments/ directory (name collisions get a -2/-3 suffix), keyed by
212
+ * `${position}:${filename}`. Written after the attachments are linked;
213
+ * reinjected into LLM-facing content on history reload.
214
+ */
215
+ attachmentStoredPaths: z.record(z.string(), z.string()).optional(),
209
216
  memoryInjectedBlock: z.string().optional(),
210
217
  /** Memory-v3 frozen net-new card block (unwrapped) — the v3 counterpart
211
218
  * of `memoryInjectedBlock`. A row carries at most one of the two. The key
@@ -333,6 +340,23 @@ export function extractImageSourcePaths(
333
340
  return Object.keys(paths).length > 0 ? paths : undefined;
334
341
  }
335
342
 
343
+ /** Extract resolved stored paths from linked attachments for message metadata. */
344
+ export function extractAttachmentStoredPaths(
345
+ attachments: ReadonlyArray<{
346
+ filename: string;
347
+ storedPath?: string;
348
+ }>,
349
+ ): Record<string, string> | undefined {
350
+ const paths: Record<string, string> = {};
351
+ for (let i = 0; i < attachments.length; i++) {
352
+ const a = attachments[i];
353
+ if (a.storedPath) {
354
+ paths[`${i}:${a.filename}`] = a.storedPath;
355
+ }
356
+ }
357
+ return Object.keys(paths).length > 0 ? paths : undefined;
358
+ }
359
+
336
360
  export interface ConversationRow {
337
361
  id: string;
338
362
  title: string | null;
@@ -134,8 +134,11 @@ export interface ModelProfileInfo {
134
134
  * - `uninstall` — the plugin's directory was removed at runtime.
135
135
  * - `disable` — the plugin was disabled at runtime (e.g. a `.disabled`
136
136
  * sentinel was added, or a feature flag turned it off).
137
+ * - `reload` — a source file inside the plugin directory changed and the
138
+ * plugin is being redeployed in place; the old version's `shutdown` runs
139
+ * before the new version is imported and its `init` fires.
137
140
  */
138
- export type ShutdownReason = "shutdown" | "uninstall" | "disable";
141
+ export type ShutdownReason = "shutdown" | "uninstall" | "disable" | "reload";
139
142
 
140
143
  /**
141
144
  * Context passed to the `shutdown` hook. Kept intentionally narrower than