@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.
- package/openapi.yaml +43 -6
- package/package.json +1 -1
- package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
- package/src/__tests__/attachments-store.test.ts +37 -1
- package/src/__tests__/attachments.test.ts +53 -0
- package/src/__tests__/channel-reply-delivery.test.ts +214 -5
- package/src/__tests__/config-loader-backfill.test.ts +16 -36
- package/src/__tests__/image-source-path-reinject.test.ts +155 -14
- package/src/__tests__/managed-profile-guard.test.ts +2 -0
- package/src/__tests__/mtime-cache.test.ts +265 -80
- package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
- package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
- package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
- package/src/agent/attachments.ts +55 -13
- package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
- package/src/cli/commands/platform/index.ts +86 -4
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
- package/src/cli/lib/diff-plugin.ts +7 -3
- package/src/cli/lib/inspect-plugin.ts +28 -10
- package/src/cli/lib/install-from-github.ts +69 -24
- package/src/cli/lib/ipc-params.ts +3 -3
- package/src/cli/lib/list-installed-plugins.ts +11 -5
- package/src/cli/lib/merge-plugin-tree.ts +15 -9
- package/src/cli/lib/plugin-fingerprint.ts +0 -6
- package/src/cli/lib/publish-plugin.ts +1 -1
- package/src/cli/lib/unknown-command.ts +3 -1
- package/src/cli/lib/upgrade-plugin.ts +2 -5
- package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
- package/src/config/seed-inference-profiles.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +37 -15
- package/src/daemon/conversation-messaging.ts +56 -28
- package/src/daemon/conversation.ts +2 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
- package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
- package/src/hooks/hook-loader.ts +57 -144
- package/src/hooks/registry.ts +13 -9
- package/src/persistence/attachments-store.ts +2 -1
- package/src/persistence/conversation-crud.ts +24 -0
- package/src/plugin-api/types.ts +4 -1
- package/src/plugins/mtime-cache.ts +347 -33
- package/src/plugins/pipeline.ts +3 -2
- package/src/providers/__tests__/inference.test.ts +42 -31
- package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
- package/src/providers/inference/auth.ts +1 -2
- package/src/providers/inference/backfill.ts +7 -5
- package/src/providers/inference/connections.ts +54 -30
- package/src/runtime/channel-reply-delivery.ts +83 -24
- package/src/runtime/channel-retry-sweep.ts +4 -0
- package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
- package/src/runtime/routes/conversation-query-routes.ts +18 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
- package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
- package/src/runtime/routes/platform-routes.ts +82 -1
- package/src/runtime/slack-reply-session.ts +1 -1
- package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
- package/src/workspace/migrations/registry.ts +2 -0
package/src/hooks/hook-loader.ts
CHANGED
|
@@ -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
|
|
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
|
|
19
|
-
*
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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
|
|
158
|
-
*
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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 [
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
*
|
|
246
|
-
* `${ownerName}/${hookName}
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
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
|
|
269
|
-
|
|
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():
|
|
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.
|
|
543
|
-
key,
|
|
544
|
-
sourceMtime: c.sourceMtime,
|
|
545
|
-
}));
|
|
458
|
+
return Array.from(hookCache.keys());
|
|
546
459
|
}
|
package/src/hooks/registry.ts
CHANGED
|
@@ -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
|
|
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")
|
|
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
|
|
85
|
-
* (async
|
|
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 } =
|
|
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))
|
|
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
|
|
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;
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -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
|