@vellumai/assistant 0.5.2 → 0.5.3
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/skills.md +100 -0
- package/package.json +1 -1
- 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-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -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__/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-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -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/config/bundled-skills/app-builder/SKILL.md +8 -8
- 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/loader.ts +1 -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-runtime-assembly.ts +2 -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/identity.ts +12 -1
- package/src/daemon/lifecycle.ts +9 -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-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 +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-crud.ts +245 -101
- package/src/memory/db-init.ts +12 -0
- package/src/memory/indexer.ts +106 -15
- package/src/memory/job-handlers/embedding.test.ts +1 -0
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -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/index.ts +3 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +2 -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/routes/conversation-management-routes.ts +6 -0
- package/src/runtime/routes/conversation-query-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +52 -5
- 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 +2 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/schedule/schedule-store.ts +21 -0
- 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/tools/permission-checker.ts +8 -1
- 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
package/src/oauth/oauth-store.ts
CHANGED
|
@@ -274,10 +274,12 @@ export async function upsertApp(
|
|
|
274
274
|
// can detect that a concurrent caller has claimed this row. Without
|
|
275
275
|
// this, a concurrent inserter's rollback DELETE would still match on
|
|
276
276
|
// the original updatedAt and delete the row we just validated.
|
|
277
|
+
const newUpdatedAt = Date.now();
|
|
277
278
|
db.update(oauthApps)
|
|
278
|
-
.set({ updatedAt:
|
|
279
|
+
.set({ updatedAt: newUpdatedAt })
|
|
279
280
|
.where(eq(oauthApps.id, existingRow.id))
|
|
280
281
|
.run();
|
|
282
|
+
return { ...existingRow, updatedAt: newUpdatedAt };
|
|
281
283
|
}
|
|
282
284
|
if (clientSecretCredentialPath) {
|
|
283
285
|
db.update(oauthApps)
|
|
@@ -2,12 +2,15 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
5
6
|
import { getConfig } from "../config/loader.js";
|
|
6
|
-
import { resolveSkillSelector } from "../config/skills.js";
|
|
7
|
+
import { loadSkillCatalog, resolveSkillSelector } from "../config/skills.js";
|
|
8
|
+
import { indexCatalogById } from "../skills/include-graph.js";
|
|
7
9
|
import {
|
|
8
10
|
isSkillSourcePath,
|
|
9
11
|
normalizeFilePath,
|
|
10
12
|
} from "../skills/path-classifier.js";
|
|
13
|
+
import { computeTransitiveSkillVersionHash } from "../skills/transitive-version-hash.js";
|
|
11
14
|
import { computeSkillVersionHash } from "../skills/version-hash.js";
|
|
12
15
|
import type { ManifestOverride } from "../tools/execution-target.js";
|
|
13
16
|
import {
|
|
@@ -352,6 +355,34 @@ function resolveSkillIdAndHash(
|
|
|
352
355
|
}
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Check whether a skill (by id) has parsed inline command expansions.
|
|
360
|
+
* Returns false when the skill is not found in the catalog.
|
|
361
|
+
*/
|
|
362
|
+
function hasInlineExpansions(skillId: string): boolean {
|
|
363
|
+
const catalog = loadSkillCatalog();
|
|
364
|
+
const skill = catalog.find((s) => s.id === skillId);
|
|
365
|
+
return (
|
|
366
|
+
skill?.inlineCommandExpansions != null &&
|
|
367
|
+
skill.inlineCommandExpansions.length > 0
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Compute the transitive version hash for a skill, returning `undefined`
|
|
373
|
+
* when computation fails (missing includes, cycle, etc.). The permission
|
|
374
|
+
* layer falls back to the any-version candidate in that case.
|
|
375
|
+
*/
|
|
376
|
+
function computeTransitiveHashSafe(skillId: string): string | undefined {
|
|
377
|
+
try {
|
|
378
|
+
const catalog = loadSkillCatalog();
|
|
379
|
+
const index = indexCatalogById(catalog);
|
|
380
|
+
return computeTransitiveSkillVersionHash(skillId, index);
|
|
381
|
+
} catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
355
386
|
function canonicalizeWebFetchUrl(parsed: URL): URL {
|
|
356
387
|
parsed.hash = "";
|
|
357
388
|
parsed.username = "";
|
|
@@ -433,13 +464,39 @@ async function buildCommandCandidates(
|
|
|
433
464
|
targets.push("");
|
|
434
465
|
} else {
|
|
435
466
|
const resolved = resolveSkillIdAndHash(rawSelector);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
|
|
468
|
+
// When the resolved skill contains inline command expansions and the
|
|
469
|
+
// feature flag is on, emit skill_load_dynamic: candidates so the
|
|
470
|
+
// higher-priority default ask rule catches them instead of falling
|
|
471
|
+
// through to the permissive skill_load:* allow rule.
|
|
472
|
+
const config = getConfig();
|
|
473
|
+
const inlineEnabled = isAssistantFeatureFlagEnabled(
|
|
474
|
+
"feature_flags.inline-skill-commands.enabled",
|
|
475
|
+
config,
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (resolved && inlineEnabled && hasInlineExpansions(resolved.id)) {
|
|
479
|
+
const transitiveHash = computeTransitiveHashSafe(resolved.id);
|
|
480
|
+
if (transitiveHash) {
|
|
481
|
+
targets.push(`skill_load_dynamic:${resolved.id}@${transitiveHash}`);
|
|
482
|
+
}
|
|
483
|
+
targets.push(`skill_load_dynamic:${resolved.id}`);
|
|
484
|
+
// Don't fall through to skill_load:* — dynamic skills use their own
|
|
485
|
+
// candidate namespace so the default ask rule applies.
|
|
486
|
+
} else {
|
|
487
|
+
if (resolved && resolved.versionHash) {
|
|
488
|
+
// Version-specific candidate lets rules pin to an exact skill version
|
|
489
|
+
targets.push(`${resolved.id}@${resolved.versionHash}`);
|
|
490
|
+
}
|
|
491
|
+
targets.push(rawSelector);
|
|
439
492
|
}
|
|
440
|
-
targets.push(rawSelector);
|
|
441
493
|
}
|
|
442
|
-
|
|
494
|
+
|
|
495
|
+
// Dynamic candidates use skill_load_dynamic: prefix; normal ones use skill_load:
|
|
496
|
+
return [...new Set(targets)].map((target) => {
|
|
497
|
+
if (target.startsWith("skill_load_dynamic:")) return target;
|
|
498
|
+
return `${toolName}:${target}`;
|
|
499
|
+
});
|
|
443
500
|
}
|
|
444
501
|
|
|
445
502
|
if (
|
|
@@ -1084,6 +1141,32 @@ function skillLoadAllowlistStrategy(
|
|
|
1084
1141
|
|
|
1085
1142
|
if (rawSelector) {
|
|
1086
1143
|
const resolved = resolveSkillIdAndHash(rawSelector);
|
|
1144
|
+
|
|
1145
|
+
// Check whether this is a dynamic (inline-command) skill load
|
|
1146
|
+
const config = getConfig();
|
|
1147
|
+
const inlineEnabled = isAssistantFeatureFlagEnabled(
|
|
1148
|
+
"feature_flags.inline-skill-commands.enabled",
|
|
1149
|
+
config,
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
if (resolved && inlineEnabled && hasInlineExpansions(resolved.id)) {
|
|
1153
|
+
const transitiveHash = computeTransitiveHashSafe(resolved.id);
|
|
1154
|
+
const options: AllowlistOption[] = [];
|
|
1155
|
+
if (transitiveHash) {
|
|
1156
|
+
options.push({
|
|
1157
|
+
label: `${resolved.id}@${transitiveHash}`,
|
|
1158
|
+
description: "This exact version (pinned)",
|
|
1159
|
+
pattern: `skill_load_dynamic:${resolved.id}@${transitiveHash}`,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
options.push({
|
|
1163
|
+
label: resolved.id,
|
|
1164
|
+
description: "This skill (any version)",
|
|
1165
|
+
pattern: `skill_load_dynamic:${resolved.id}`,
|
|
1166
|
+
});
|
|
1167
|
+
return options;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1087
1170
|
if (resolved && resolved.versionHash) {
|
|
1088
1171
|
return [
|
|
1089
1172
|
{
|
|
@@ -198,6 +198,19 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
198
198
|
})),
|
|
199
199
|
);
|
|
200
200
|
|
|
201
|
+
// Inline-command skill loads use a distinct candidate namespace
|
|
202
|
+
// (skill_load_dynamic:*) so they prompt by default instead of falling
|
|
203
|
+
// through to the permissive skill_load:* allow rule below. The higher
|
|
204
|
+
// priority ensures this rule wins when both could match.
|
|
205
|
+
const skillLoadDynamicRule: DefaultRuleTemplate = {
|
|
206
|
+
id: "default:ask-skill_load_dynamic-global",
|
|
207
|
+
tool: "skill_load",
|
|
208
|
+
pattern: "skill_load_dynamic:*",
|
|
209
|
+
scope: "everywhere",
|
|
210
|
+
decision: "ask",
|
|
211
|
+
priority: 200,
|
|
212
|
+
};
|
|
213
|
+
|
|
201
214
|
const skillLoadRule: DefaultRuleTemplate = {
|
|
202
215
|
id: "default:allow-skill_load-global",
|
|
203
216
|
tool: "skill_load",
|
|
@@ -294,6 +307,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
294
307
|
bootstrapDeleteRule,
|
|
295
308
|
updatesDeleteRule,
|
|
296
309
|
...skillSourceMutationRules,
|
|
310
|
+
skillLoadDynamicRule,
|
|
297
311
|
skillLoadRule,
|
|
298
312
|
skillExecuteRule,
|
|
299
313
|
browserNavigateRule,
|
|
@@ -275,6 +275,12 @@ export function conversationManagementRouteDefinitions(
|
|
|
275
275
|
targetId: itemId,
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
|
+
for (const summaryId of deleted.deletedSummaryIds) {
|
|
279
|
+
enqueueMemoryJob("delete_qdrant_vectors", {
|
|
280
|
+
targetType: "summary",
|
|
281
|
+
targetId: summaryId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
278
284
|
log.info({ conversationId: resolvedId }, "Deleted conversation");
|
|
279
285
|
return new Response(null, { status: 204 });
|
|
280
286
|
},
|
|
@@ -225,6 +225,13 @@ export function conversationQueryRouteDefinitions(
|
|
|
225
225
|
400,
|
|
226
226
|
);
|
|
227
227
|
}
|
|
228
|
+
if (body.model !== undefined && typeof body.model !== "string") {
|
|
229
|
+
return httpError(
|
|
230
|
+
"BAD_REQUEST",
|
|
231
|
+
"Field 'model' must be a string",
|
|
232
|
+
400,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
228
235
|
try {
|
|
229
236
|
const info = await setEmbeddingConfig(
|
|
230
237
|
body.provider,
|
|
@@ -81,6 +81,9 @@ import {
|
|
|
81
81
|
|
|
82
82
|
const log = getLogger("conversation-routes");
|
|
83
83
|
|
|
84
|
+
/** Matches the `<no_response/>` sentinel used by channel delivery suppression. */
|
|
85
|
+
const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
|
|
86
|
+
|
|
84
87
|
const SUGGESTION_CACHE_MAX = 100;
|
|
85
88
|
|
|
86
89
|
function collectCanonicalGuardianRequestHintIds(
|
|
@@ -363,6 +366,48 @@ export function handleListMessages(
|
|
|
363
366
|
content = msg.content;
|
|
364
367
|
}
|
|
365
368
|
const rendered = renderHistoryContent(content);
|
|
369
|
+
|
|
370
|
+
// Strip <no_response/> markers from assistant messages so web/API
|
|
371
|
+
// clients never see the raw sentinel. Only assistant messages produce
|
|
372
|
+
// this marker; user messages are left untouched.
|
|
373
|
+
if (msg.role === "assistant") {
|
|
374
|
+
const originalSegments = rendered.textSegments;
|
|
375
|
+
const keepIndices: number[] = [];
|
|
376
|
+
const filteredSegments: string[] = [];
|
|
377
|
+
for (let i = 0; i < originalSegments.length; i++) {
|
|
378
|
+
const cleaned = originalSegments[i]
|
|
379
|
+
.replace(NO_RESPONSE_INLINE_RE, "")
|
|
380
|
+
.trim();
|
|
381
|
+
if (cleaned.length > 0) {
|
|
382
|
+
keepIndices.push(i);
|
|
383
|
+
filteredSegments.push(cleaned);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Remap contentOrder text:N indices to account for removed segments
|
|
387
|
+
const indexMap = new Map<number, number>();
|
|
388
|
+
keepIndices.forEach((oldIdx, newIdx) => indexMap.set(oldIdx, newIdx));
|
|
389
|
+
const filteredContentOrder = rendered.contentOrder
|
|
390
|
+
.map((entry) => {
|
|
391
|
+
const m = entry.match(/^text:(\d+)$/);
|
|
392
|
+
if (!m) return entry;
|
|
393
|
+
const newIdx = indexMap.get(Number(m[1]));
|
|
394
|
+
return newIdx !== undefined ? `text:${newIdx}` : undefined;
|
|
395
|
+
})
|
|
396
|
+
.filter((e): e is string => e !== undefined);
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
role: msg.role,
|
|
400
|
+
text: rendered.text.replace(NO_RESPONSE_INLINE_RE, "").trim(),
|
|
401
|
+
timestamp: msg.createdAt,
|
|
402
|
+
toolCalls: rendered.toolCalls,
|
|
403
|
+
toolCallsBeforeText: rendered.toolCallsBeforeText,
|
|
404
|
+
textSegments: filteredSegments,
|
|
405
|
+
contentOrder: filteredContentOrder,
|
|
406
|
+
surfaces: rendered.surfaces,
|
|
407
|
+
id: msg.id,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
366
411
|
return {
|
|
367
412
|
role: msg.role,
|
|
368
413
|
text: rendered.text,
|
|
@@ -1240,11 +1285,13 @@ async function generateLlmSuggestion(
|
|
|
1240
1285
|
return null;
|
|
1241
1286
|
}
|
|
1242
1287
|
if (firstLine.length <= 50) return firstLine;
|
|
1243
|
-
// Truncate at last word boundary within 50 chars
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1288
|
+
// Truncate at last word boundary within 50 chars.
|
|
1289
|
+
// Only strip the trailing partial word if the slice actually cut mid-word;
|
|
1290
|
+
// if the character right after the cut is whitespace, the slice is already clean.
|
|
1291
|
+
const sliced = firstLine.slice(0, 50);
|
|
1292
|
+
const wordTruncated = (
|
|
1293
|
+
/\s/.test(firstLine[50]) ? sliced : sliced.replace(/\s+\S*$/, "")
|
|
1294
|
+
).trim();
|
|
1248
1295
|
if (wordTruncated.length < 15) {
|
|
1249
1296
|
log.debug(
|
|
1250
1297
|
{ rawLength: firstLine.length, truncatedLength: wordTruncated.length },
|
|
@@ -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";
|
|
@@ -346,6 +347,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
|
|
|
346
347
|
500,
|
|
347
348
|
);
|
|
348
349
|
}
|
|
350
|
+
clearEmbeddingBackendCache();
|
|
349
351
|
invalidateConfigCache();
|
|
350
352
|
await initializeProviders(getConfig());
|
|
351
353
|
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
|
},
|
|
@@ -202,6 +202,27 @@ export function listSchedules(options?: {
|
|
|
202
202
|
return rows.map(parseJobRow);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Return enabled schedules whose next run falls within a time window.
|
|
207
|
+
* Used by the memory brief compiler to surface due-soon schedule entries.
|
|
208
|
+
*/
|
|
209
|
+
export function getDueSoonSchedules(
|
|
210
|
+
now: number,
|
|
211
|
+
horizonMs: number,
|
|
212
|
+
): ScheduleJob[] {
|
|
213
|
+
const db = getDb();
|
|
214
|
+
const cutoff = now + horizonMs;
|
|
215
|
+
const rows = db
|
|
216
|
+
.select()
|
|
217
|
+
.from(scheduleJobs)
|
|
218
|
+
.where(
|
|
219
|
+
and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, cutoff)),
|
|
220
|
+
)
|
|
221
|
+
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
222
|
+
.all();
|
|
223
|
+
return rows.map(parseJobRow);
|
|
224
|
+
}
|
|
225
|
+
|
|
205
226
|
export function updateSchedule(
|
|
206
227
|
id: string,
|
|
207
228
|
updates: {
|