@vellumai/assistant 0.5.2 → 0.5.4
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/ARCHITECTURE.md +109 -0
- package/docs/architecture/memory.md +105 -0
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +2 -2
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +707 -0
- package/src/__tests__/memory-reducer.test.ts +704 -0
- package/src/__tests__/memory-regressions.test.ts +30 -8
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +4 -4
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +8 -8
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/raw-config-utils.ts +28 -0
- package/src/config/schema.ts +12 -0
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/skills.ts +50 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -3
- package/src/daemon/conversation-agent-loop.ts +71 -1
- package/src/daemon/conversation-lifecycle.ts +11 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +3 -1
- package/src/daemon/conversation-surfaces.ts +31 -8
- package/src/daemon/conversation.ts +40 -23
- package/src/daemon/handlers/config-embeddings.ts +10 -2
- package/src/daemon/handlers/config-model.ts +0 -9
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/lifecycle.ts +52 -1
- package/src/daemon/message-types/conversations.ts +0 -1
- package/src/daemon/server.ts +1 -1
- package/src/followups/followup-store.ts +47 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +162 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-crud.ts +455 -101
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +106 -15
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +9 -3
- package/src/memory/job-handlers/embedding.test.ts +1 -0
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +8 -0
- package/src/memory/jobs-worker.ts +20 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +106 -0
- package/src/memory/reducer.ts +467 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/oauth/oauth-store.ts +3 -1
- package/src/permissions/checker.ts +89 -6
- package/src/permissions/defaults.ts +14 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/routes/conversation-management-routes.ts +94 -2
- package/src/runtime/routes/conversation-query-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +52 -5
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/llm-context-normalization.ts +14 -1
- package/src/runtime/routes/memory-item-routes.ts +90 -5
- package/src/runtime/routes/secret-routes.ts +3 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/schedule/schedule-store.ts +28 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/tasks/task-store.ts +43 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -1
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/skills/load.ts +140 -6
- package/src/util/platform.ts +18 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +1 -1
- package/src/workspace/migrations/registry.ts +1 -1
|
@@ -28,6 +28,7 @@ type ServerWithRequestIP = {
|
|
|
28
28
|
): { address: string; family: string; port: number } | null;
|
|
29
29
|
};
|
|
30
30
|
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
31
|
+
import { getIsContainerized } from "../../config/env-registry.js";
|
|
31
32
|
|
|
32
33
|
const log = getLogger("guardian-bootstrap");
|
|
33
34
|
|
|
@@ -86,19 +87,30 @@ export async function handleGuardianBootstrap(
|
|
|
86
87
|
req: Request,
|
|
87
88
|
server: ServerWithRequestIP,
|
|
88
89
|
): Promise<Response> {
|
|
90
|
+
// Reject non-private-network peers (allows loopback, Docker bridge, etc.)
|
|
91
|
+
const peerIp = server.requestIP(req)?.address;
|
|
92
|
+
if ((!peerIp || !isPrivateAddress(peerIp)) && !isHttpAuthDisabled()) {
|
|
93
|
+
return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
// Reject requests forwarded from public networks. The gateway sets
|
|
90
97
|
// x-forwarded-for to the real client IP; if that IP is on a private
|
|
91
98
|
// network (loopback, Docker bridge, RFC 1918) the request is still
|
|
92
99
|
// considered local. Only reject when the forwarded IP is public.
|
|
100
|
+
//
|
|
101
|
+
// Skip this check when running in a container: the peer IP was already
|
|
102
|
+
// validated above (Docker bridge network = private), so the request
|
|
103
|
+
// reached us through a co-located gateway. The x-forwarded-for header
|
|
104
|
+
// reflects the original external client (e.g. platform proxy) and is
|
|
105
|
+
// not meaningful for local-only enforcement in this topology.
|
|
93
106
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
94
107
|
const forwardedIp = forwarded ? forwarded.split(",")[0].trim() : null;
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if ((!peerIp || !isPrivateAddress(peerIp)) && !isHttpAuthDisabled()) {
|
|
108
|
+
if (
|
|
109
|
+
forwardedIp &&
|
|
110
|
+
!isPrivateAddress(forwardedIp) &&
|
|
111
|
+
!isHttpAuthDisabled() &&
|
|
112
|
+
!getIsContainerized()
|
|
113
|
+
) {
|
|
102
114
|
return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
|
|
103
115
|
}
|
|
104
116
|
|
|
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
import { getBaseDataDir } from "../../config/env-registry.js";
|
|
11
|
+
import { parseIdentityFields } from "../../daemon/handlers/identity.js";
|
|
11
12
|
import { getWorkspacePromptPath, readLockfile } from "../../util/platform.js";
|
|
12
13
|
import { httpError } from "../http-errors.js";
|
|
13
14
|
import type { RouteDefinition } from "../http-router.js";
|
|
@@ -149,41 +150,7 @@ export function handleGetIdentity(): Response {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
const content = readFileSync(identityPath, "utf-8");
|
|
152
|
-
const fields
|
|
153
|
-
for (const line of content.split("\n")) {
|
|
154
|
-
const trimmed = line.trim();
|
|
155
|
-
const lower = trimmed.toLowerCase();
|
|
156
|
-
const extract = (prefix: string): string | null => {
|
|
157
|
-
if (!lower.startsWith(prefix)) return null;
|
|
158
|
-
return trimmed.split(":**").pop()?.trim() ?? null;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const name = extract("- **name:**");
|
|
162
|
-
if (name) {
|
|
163
|
-
fields.name = name;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const role = extract("- **role:**");
|
|
167
|
-
if (role) {
|
|
168
|
-
fields.role = role;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const personality = extract("- **personality:**") ?? extract("- **vibe:**");
|
|
172
|
-
if (personality) {
|
|
173
|
-
fields.personality = personality;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
const emoji = extract("- **emoji:**");
|
|
177
|
-
if (emoji) {
|
|
178
|
-
fields.emoji = emoji;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
const home = extract("- **home:**");
|
|
182
|
-
if (home) {
|
|
183
|
-
fields.home = home;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
153
|
+
const fields = parseIdentityFields(content);
|
|
187
154
|
|
|
188
155
|
const version = getPackageVersion();
|
|
189
156
|
|
|
@@ -86,7 +86,8 @@ export function normalizeLlmContextPayloads(
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (requestCandidate) {
|
|
89
|
-
|
|
89
|
+
const { summary, requestSections, responseSections } = requestCandidate;
|
|
90
|
+
return { summary, requestSections, responseSections };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
if (responseCandidate) {
|
|
@@ -123,6 +124,7 @@ function normalizeOpenAiRequestPayload(
|
|
|
123
124
|
|
|
124
125
|
const requestToolNames = extractOpenAiRequestToolNames(request.tools);
|
|
125
126
|
const hasOpenAiSignal =
|
|
127
|
+
hasOpenAiModelPrefix(asString(request.model)) ||
|
|
126
128
|
requestToolNames.length > 0 ||
|
|
127
129
|
asString(request.tool_choice) !== undefined ||
|
|
128
130
|
(request.parallel_tool_calls !== undefined &&
|
|
@@ -291,6 +293,7 @@ function normalizeAnthropicRequestPayload(
|
|
|
291
293
|
}),
|
|
292
294
|
);
|
|
293
295
|
const hasAnthropicSignal =
|
|
296
|
+
hasAnthropicModelPrefix(asString(request.model)) ||
|
|
294
297
|
request.system !== undefined ||
|
|
295
298
|
requestToolNames.length > 0 ||
|
|
296
299
|
isAnthropicToolChoice(request.tool_choice) ||
|
|
@@ -1171,6 +1174,16 @@ function normalizeCompatibleRequestPayload(
|
|
|
1171
1174
|
}
|
|
1172
1175
|
}
|
|
1173
1176
|
|
|
1177
|
+
function hasOpenAiModelPrefix(model: string | undefined): boolean {
|
|
1178
|
+
if (!model) return false;
|
|
1179
|
+
return /^(gpt-|chatgpt-|ft:|o[1-9]\d*(-|$))/.test(model);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function hasAnthropicModelPrefix(model: string | undefined): boolean {
|
|
1183
|
+
if (!model) return false;
|
|
1184
|
+
return model.startsWith("claude-");
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1174
1187
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
1175
1188
|
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
1176
1189
|
return null;
|
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
* DELETE /v1/memory-items/:id — delete a memory item and its embeddings
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { and, asc, count, desc, eq, like, ne, or } from "drizzle-orm";
|
|
11
|
+
import { and, asc, count, desc, eq, inArray, like, ne, or } from "drizzle-orm";
|
|
12
12
|
import { v4 as uuid } from "uuid";
|
|
13
13
|
|
|
14
14
|
import { getDb } from "../../memory/db.js";
|
|
15
15
|
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
16
16
|
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
conversations,
|
|
19
|
+
memoryEmbeddings,
|
|
20
|
+
memoryItems,
|
|
21
|
+
} from "../../memory/schema.js";
|
|
18
22
|
import { truncate } from "../../util/truncate.js";
|
|
19
23
|
import { httpError } from "../http-errors.js";
|
|
20
24
|
import type { RouteContext, RouteDefinition } from "../http-router.js";
|
|
@@ -64,6 +68,54 @@ function isValidSortField(value: string): value is SortField {
|
|
|
64
68
|
return (VALID_SORT_FIELDS as readonly string[]).includes(value);
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a `scopeLabel` for a memory item based on its `scopeId`.
|
|
73
|
+
*
|
|
74
|
+
* - `"default"` → `null`
|
|
75
|
+
* - `"private:<conversationId>"` → `"Private · <title>"` when the conversation
|
|
76
|
+
* has a title, or `"Private"` when it doesn't (or the conversation was deleted).
|
|
77
|
+
*/
|
|
78
|
+
function resolveScopeLabel(
|
|
79
|
+
scopeId: string,
|
|
80
|
+
titleMap: Map<string, string | null>,
|
|
81
|
+
): string | null {
|
|
82
|
+
if (scopeId === "default") return null;
|
|
83
|
+
if (scopeId.startsWith("private:")) {
|
|
84
|
+
const conversationId = scopeId.slice("private:".length);
|
|
85
|
+
const title = titleMap.get(conversationId);
|
|
86
|
+
return title ? `Private · ${title}` : "Private";
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Batch-fetch conversation titles for a set of private-scoped memory items.
|
|
93
|
+
* Returns a Map from conversation ID → title (or null).
|
|
94
|
+
*/
|
|
95
|
+
function buildConversationTitleMap(
|
|
96
|
+
db: ReturnType<typeof getDb>,
|
|
97
|
+
scopeIds: string[],
|
|
98
|
+
): Map<string, string | null> {
|
|
99
|
+
const conversationIds = scopeIds
|
|
100
|
+
.filter((s) => s.startsWith("private:"))
|
|
101
|
+
.map((s) => s.slice("private:".length));
|
|
102
|
+
|
|
103
|
+
const uniqueIds = [...new Set(conversationIds)];
|
|
104
|
+
if (uniqueIds.length === 0) return new Map();
|
|
105
|
+
|
|
106
|
+
const rows = db
|
|
107
|
+
.select({ id: conversations.id, title: conversations.title })
|
|
108
|
+
.from(conversations)
|
|
109
|
+
.where(inArray(conversations.id, uniqueIds))
|
|
110
|
+
.all();
|
|
111
|
+
|
|
112
|
+
const map = new Map<string, string | null>();
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
map.set(row.id, row.title);
|
|
115
|
+
}
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
118
|
+
|
|
67
119
|
// ---------------------------------------------------------------------------
|
|
68
120
|
// GET /v1/memory-items
|
|
69
121
|
// ---------------------------------------------------------------------------
|
|
@@ -145,7 +197,17 @@ export function handleListMemoryItems(url: URL): Response {
|
|
|
145
197
|
.offset(offsetParam)
|
|
146
198
|
.all();
|
|
147
199
|
|
|
148
|
-
|
|
200
|
+
// Resolve scope labels for private-scoped items
|
|
201
|
+
const titleMap = buildConversationTitleMap(
|
|
202
|
+
db,
|
|
203
|
+
items.map((i) => i.scopeId),
|
|
204
|
+
);
|
|
205
|
+
const enrichedItems = items.map((item) => ({
|
|
206
|
+
...item,
|
|
207
|
+
scopeLabel: resolveScopeLabel(item.scopeId, titleMap),
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
return Response.json({ items: enrichedItems, total });
|
|
149
211
|
}
|
|
150
212
|
|
|
151
213
|
// ---------------------------------------------------------------------------
|
|
@@ -187,9 +249,14 @@ export function handleGetMemoryItem(ctx: RouteContext): Response {
|
|
|
187
249
|
supersededBySubject = superseding?.subject;
|
|
188
250
|
}
|
|
189
251
|
|
|
252
|
+
// Resolve scope label
|
|
253
|
+
const titleMap = buildConversationTitleMap(db, [item.scopeId]);
|
|
254
|
+
const scopeLabel = resolveScopeLabel(item.scopeId, titleMap);
|
|
255
|
+
|
|
190
256
|
return Response.json({
|
|
191
257
|
item: {
|
|
192
258
|
...item,
|
|
259
|
+
scopeLabel,
|
|
193
260
|
...(supersedesSubject !== undefined ? { supersedesSubject } : {}),
|
|
194
261
|
...(supersededBySubject !== undefined ? { supersededBySubject } : {}),
|
|
195
262
|
},
|
|
@@ -303,7 +370,14 @@ export async function handleCreateMemoryItem(
|
|
|
303
370
|
.where(eq(memoryItems.id, id))
|
|
304
371
|
.get();
|
|
305
372
|
|
|
306
|
-
|
|
373
|
+
// Enrich with scopeLabel for API consistency
|
|
374
|
+
const titleMap = buildConversationTitleMap(db, [scopeId]);
|
|
375
|
+
const scopeLabel = resolveScopeLabel(scopeId, titleMap);
|
|
376
|
+
|
|
377
|
+
return Response.json(
|
|
378
|
+
{ item: { ...insertedRow, scopeLabel } },
|
|
379
|
+
{ status: 201 },
|
|
380
|
+
);
|
|
307
381
|
}
|
|
308
382
|
|
|
309
383
|
// ---------------------------------------------------------------------------
|
|
@@ -430,7 +504,18 @@ export async function handleUpdateMemoryItem(
|
|
|
430
504
|
.where(eq(memoryItems.id, id))
|
|
431
505
|
.get();
|
|
432
506
|
|
|
433
|
-
|
|
507
|
+
// Enrich with scopeLabel for API consistency
|
|
508
|
+
const patchTitleMap = buildConversationTitleMap(db, [
|
|
509
|
+
updatedRow?.scopeId ?? existing.scopeId,
|
|
510
|
+
]);
|
|
511
|
+
const patchScopeLabel = resolveScopeLabel(
|
|
512
|
+
updatedRow?.scopeId ?? existing.scopeId,
|
|
513
|
+
patchTitleMap,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
return Response.json({
|
|
517
|
+
item: { ...updatedRow, scopeLabel: patchScopeLabel },
|
|
518
|
+
});
|
|
434
519
|
}
|
|
435
520
|
|
|
436
521
|
// ---------------------------------------------------------------------------
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../../config/loader.js";
|
|
12
12
|
import type { CesClient } from "../../credential-execution/client.js";
|
|
13
13
|
import { setSentryOrganizationId } from "../../instrument.js";
|
|
14
|
+
import { clearEmbeddingBackendCache } from "../../memory/embedding-backend.js";
|
|
14
15
|
import { syncManualTokenConnection } from "../../oauth/manual-token-connection.js";
|
|
15
16
|
import { validateAnthropicApiKey } from "../../providers/anthropic/client.js";
|
|
16
17
|
import { validateGeminiApiKey } from "../../providers/gemini/client.js";
|
|
@@ -181,6 +182,7 @@ export async function handleAddSecret(
|
|
|
181
182
|
500,
|
|
182
183
|
);
|
|
183
184
|
}
|
|
185
|
+
clearEmbeddingBackendCache();
|
|
184
186
|
invalidateConfigCache();
|
|
185
187
|
await initializeProviders(getConfig());
|
|
186
188
|
log.info({ provider: name }, "API key updated via HTTP");
|
|
@@ -346,6 +348,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
346
348
|
500,
|
|
347
349
|
);
|
|
348
350
|
}
|
|
351
|
+
clearEmbeddingBackendCache();
|
|
349
352
|
invalidateConfigCache();
|
|
350
353
|
await initializeProviders(getConfig());
|
|
351
354
|
log.info({ provider: name }, "API key deleted via HTTP");
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* Requires the conversation to already exist (does not create new conversations).
|
|
6
6
|
*/
|
|
7
7
|
import { getLogger } from "../../util/logger.js";
|
|
8
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
9
|
+
import type { AuthContext } from "../auth/types.js";
|
|
10
|
+
import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
|
|
8
11
|
import { httpError } from "../http-errors.js";
|
|
9
12
|
import type { RouteDefinition } from "../http-router.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveTrustContext,
|
|
15
|
+
withSourceChannel,
|
|
16
|
+
} from "../trust-context-resolver.js";
|
|
10
17
|
|
|
11
18
|
const log = getLogger("surface-action-routes");
|
|
12
19
|
|
|
@@ -18,6 +25,11 @@ interface SurfaceActionTarget {
|
|
|
18
25
|
data?: Record<string, unknown>,
|
|
19
26
|
): void;
|
|
20
27
|
handleSurfaceUndo?(surfaceId: string): void;
|
|
28
|
+
setTrustContext?(ctx: {
|
|
29
|
+
trustClass: "guardian" | "trusted_contact" | "unknown";
|
|
30
|
+
sourceChannel: string;
|
|
31
|
+
}): void;
|
|
32
|
+
trustContext?: { trustClass: string } | null;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
export type ConversationLookup = (
|
|
@@ -28,6 +40,53 @@ export type ConversationLookupBySurfaceId = (
|
|
|
28
40
|
surfaceId: string,
|
|
29
41
|
) => SurfaceActionTarget | undefined;
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Resolve trust context from the request's auth context and set it on the
|
|
45
|
+
* conversation, following the same pattern as POST /v1/messages. This ensures
|
|
46
|
+
* surface actions inherit the correct trust class (guardian vs trusted_contact)
|
|
47
|
+
* rather than defaulting to unknown.
|
|
48
|
+
*/
|
|
49
|
+
function applyTrustContext(
|
|
50
|
+
conversation: SurfaceActionTarget,
|
|
51
|
+
authContext: AuthContext,
|
|
52
|
+
): void {
|
|
53
|
+
if (!conversation.setTrustContext) return;
|
|
54
|
+
|
|
55
|
+
const sourceChannel = "vellum";
|
|
56
|
+
|
|
57
|
+
if (authContext.actorPrincipalId) {
|
|
58
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
59
|
+
let trustCtx = resolveTrustContext({
|
|
60
|
+
assistantId,
|
|
61
|
+
sourceChannel,
|
|
62
|
+
conversationExternalId: "local",
|
|
63
|
+
actorExternalId: authContext.actorPrincipalId,
|
|
64
|
+
});
|
|
65
|
+
if (trustCtx.trustClass === "unknown") {
|
|
66
|
+
const healed = healGuardianBindingDrift(authContext.actorPrincipalId);
|
|
67
|
+
if (healed) {
|
|
68
|
+
trustCtx = resolveTrustContext({
|
|
69
|
+
assistantId,
|
|
70
|
+
sourceChannel,
|
|
71
|
+
conversationExternalId: "local",
|
|
72
|
+
actorExternalId: authContext.actorPrincipalId,
|
|
73
|
+
});
|
|
74
|
+
log.info(
|
|
75
|
+
{
|
|
76
|
+
actorPrincipalId: authContext.actorPrincipalId,
|
|
77
|
+
trustClass: trustCtx.trustClass,
|
|
78
|
+
},
|
|
79
|
+
"Trust re-resolved after guardian binding drift heal (surface action)",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
conversation.setTrustContext(withSourceChannel(sourceChannel, trustCtx));
|
|
84
|
+
} else {
|
|
85
|
+
// Service principals or tokens without an actor ID get guardian context.
|
|
86
|
+
conversation.setTrustContext({ trustClass: "guardian", sourceChannel });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
31
90
|
/**
|
|
32
91
|
* POST /v1/surface-actions — handle a UI surface action.
|
|
33
92
|
*
|
|
@@ -37,6 +96,7 @@ export async function handleSurfaceAction(
|
|
|
37
96
|
req: Request,
|
|
38
97
|
findConversation: ConversationLookup,
|
|
39
98
|
findConversationBySurfaceId?: ConversationLookupBySurfaceId,
|
|
99
|
+
authContext?: AuthContext,
|
|
40
100
|
): Promise<Response> {
|
|
41
101
|
const body = (await req.json()) as {
|
|
42
102
|
conversationId?: string | null;
|
|
@@ -65,6 +125,12 @@ export async function handleSurfaceAction(
|
|
|
65
125
|
return httpError("NOT_FOUND", "No active conversation found", 404);
|
|
66
126
|
}
|
|
67
127
|
|
|
128
|
+
// Resolve trust context from the request's auth headers so the conversation
|
|
129
|
+
// has the correct trust class for tool approval decisions.
|
|
130
|
+
if (authContext) {
|
|
131
|
+
applyTrustContext(conversation, authContext);
|
|
132
|
+
}
|
|
133
|
+
|
|
68
134
|
try {
|
|
69
135
|
conversation.handleSurfaceAction(surfaceId, actionId, data);
|
|
70
136
|
log.info(
|
|
@@ -143,7 +209,7 @@ export function surfaceActionRouteDefinitions(deps: {
|
|
|
143
209
|
{
|
|
144
210
|
endpoint: "surface-actions",
|
|
145
211
|
method: "POST",
|
|
146
|
-
handler: async ({ req }) => {
|
|
212
|
+
handler: async ({ req, authContext }) => {
|
|
147
213
|
if (!deps.findConversation) {
|
|
148
214
|
return httpError(
|
|
149
215
|
"NOT_IMPLEMENTED",
|
|
@@ -155,6 +221,7 @@ export function surfaceActionRouteDefinitions(deps: {
|
|
|
155
221
|
req,
|
|
156
222
|
deps.findConversation,
|
|
157
223
|
deps.findConversationBySurfaceId,
|
|
224
|
+
authContext,
|
|
158
225
|
);
|
|
159
226
|
},
|
|
160
227
|
},
|
|
@@ -35,6 +35,7 @@ export interface ScheduleJob {
|
|
|
35
35
|
mode: ScheduleMode;
|
|
36
36
|
routingIntent: RoutingIntent;
|
|
37
37
|
routingHints: Record<string, unknown>;
|
|
38
|
+
quiet: boolean;
|
|
38
39
|
status: ScheduleStatus;
|
|
39
40
|
createdAt: number;
|
|
40
41
|
updatedAt: number;
|
|
@@ -91,6 +92,7 @@ export function createSchedule(params: {
|
|
|
91
92
|
mode?: ScheduleMode;
|
|
92
93
|
routingIntent?: RoutingIntent;
|
|
93
94
|
routingHints?: Record<string, unknown>;
|
|
95
|
+
quiet?: boolean;
|
|
94
96
|
}): ScheduleJob {
|
|
95
97
|
const expression = params.expression ?? params.cronExpression ?? null;
|
|
96
98
|
const isOneShot = expression == null;
|
|
@@ -118,6 +120,7 @@ export function createSchedule(params: {
|
|
|
118
120
|
const mode = params.mode ?? "execute";
|
|
119
121
|
const routingIntent = params.routingIntent ?? "all_channels";
|
|
120
122
|
const routingHints = params.routingHints ?? {};
|
|
123
|
+
const quiet = params.quiet ?? false;
|
|
121
124
|
|
|
122
125
|
let nextRunAt: number;
|
|
123
126
|
if (isOneShot) {
|
|
@@ -144,6 +147,7 @@ export function createSchedule(params: {
|
|
|
144
147
|
mode,
|
|
145
148
|
routingIntent,
|
|
146
149
|
routingHintsJson: JSON.stringify(routingHints),
|
|
150
|
+
quiet,
|
|
147
151
|
status: "active" as ScheduleStatus,
|
|
148
152
|
createdAt: now,
|
|
149
153
|
updatedAt: now,
|
|
@@ -202,6 +206,27 @@ export function listSchedules(options?: {
|
|
|
202
206
|
return rows.map(parseJobRow);
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Return enabled schedules whose next run falls within a time window.
|
|
211
|
+
* Used by the memory brief compiler to surface due-soon schedule entries.
|
|
212
|
+
*/
|
|
213
|
+
export function getDueSoonSchedules(
|
|
214
|
+
now: number,
|
|
215
|
+
horizonMs: number,
|
|
216
|
+
): ScheduleJob[] {
|
|
217
|
+
const db = getDb();
|
|
218
|
+
const cutoff = now + horizonMs;
|
|
219
|
+
const rows = db
|
|
220
|
+
.select()
|
|
221
|
+
.from(scheduleJobs)
|
|
222
|
+
.where(
|
|
223
|
+
and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, cutoff)),
|
|
224
|
+
)
|
|
225
|
+
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
226
|
+
.all();
|
|
227
|
+
return rows.map(parseJobRow);
|
|
228
|
+
}
|
|
229
|
+
|
|
205
230
|
export function updateSchedule(
|
|
206
231
|
id: string,
|
|
207
232
|
updates: {
|
|
@@ -215,6 +240,7 @@ export function updateSchedule(
|
|
|
215
240
|
mode?: ScheduleMode;
|
|
216
241
|
routingIntent?: RoutingIntent;
|
|
217
242
|
routingHints?: Record<string, unknown>;
|
|
243
|
+
quiet?: boolean;
|
|
218
244
|
},
|
|
219
245
|
): ScheduleJob | null {
|
|
220
246
|
const db = getDb();
|
|
@@ -269,6 +295,7 @@ export function updateSchedule(
|
|
|
269
295
|
set.routingIntent = updates.routingIntent;
|
|
270
296
|
if (updates.routingHints !== undefined)
|
|
271
297
|
set.routingHintsJson = JSON.stringify(updates.routingHints);
|
|
298
|
+
if (updates.quiet !== undefined) set.quiet = updates.quiet;
|
|
272
299
|
|
|
273
300
|
// Recompute nextRunAt if schedule timing may have changed (only for recurring)
|
|
274
301
|
if (
|
|
@@ -750,6 +777,7 @@ function parseJobRow(row: typeof scheduleJobs.$inferSelect): ScheduleJob {
|
|
|
750
777
|
mode: (row.mode ?? "execute") as ScheduleMode,
|
|
751
778
|
routingIntent: (row.routingIntent ?? "all_channels") as RoutingIntent,
|
|
752
779
|
routingHints: safeParseJson(row.routingHintsJson),
|
|
780
|
+
quiet: row.quiet ?? false,
|
|
753
781
|
status: (row.status ?? "active") as ScheduleStatus,
|
|
754
782
|
createdAt: row.createdAt,
|
|
755
783
|
updatedAt: row.updatedAt,
|
|
@@ -206,7 +206,9 @@ async function runScheduleOnce(
|
|
|
206
206
|
if (isOneShot) failOneShot(job.id);
|
|
207
207
|
} else {
|
|
208
208
|
completeScheduleRun(runId, { status: "ok" });
|
|
209
|
-
|
|
209
|
+
if (!job.quiet) {
|
|
210
|
+
notifySchedule({ id: job.id, name: job.name });
|
|
211
|
+
}
|
|
210
212
|
if (isOneShot) completeOneShot(job.id);
|
|
211
213
|
}
|
|
212
214
|
processed += 1;
|
|
@@ -278,7 +280,9 @@ async function runScheduleOnce(
|
|
|
278
280
|
trustClass: "guardian",
|
|
279
281
|
});
|
|
280
282
|
completeScheduleRun(runId, { status: "ok" });
|
|
281
|
-
|
|
283
|
+
if (!job.quiet) {
|
|
284
|
+
notifySchedule({ id: job.id, name: job.name });
|
|
285
|
+
}
|
|
282
286
|
if (isOneShot) completeOneShot(job.id);
|
|
283
287
|
processed += 1;
|
|
284
288
|
} catch (err) {
|