@vellumai/assistant 0.10.3 → 0.10.4-staging.1
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 +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -8,16 +8,8 @@ import {
|
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { basename, join } from "node:path";
|
|
11
|
+
import { Database } from "bun:sqlite";
|
|
11
12
|
|
|
12
|
-
import { eq } from "drizzle-orm";
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
findGuardianForChannel,
|
|
16
|
-
generateUserFileSlug,
|
|
17
|
-
listGuardianChannels,
|
|
18
|
-
} from "../../contacts/contact-store.js";
|
|
19
|
-
import { getDb } from "../../memory/db-connection.js";
|
|
20
|
-
import { contacts } from "../../memory/schema/contacts.js";
|
|
21
13
|
import { getLogger } from "../../util/logger.js";
|
|
22
14
|
import type { WorkspaceMigration } from "./types.js";
|
|
23
15
|
|
|
@@ -25,11 +17,11 @@ const log = getLogger("workspace-migration-031-drop-user-md");
|
|
|
25
17
|
|
|
26
18
|
// ── Inlined helpers ───────────────────────────────────────────────
|
|
27
19
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
20
|
+
// AGENTS.md requires each migration to be self-contained: it may import
|
|
21
|
+
// only `./types.js`, `./utils.js`, the logger, and runtime built-ins.
|
|
22
|
+
// The helpers below (including the guardian DB read and the userFile
|
|
23
|
+
// slug) are inlined so this migration does not regress if the modules
|
|
24
|
+
// they originate from change later.
|
|
33
25
|
|
|
34
26
|
/**
|
|
35
27
|
* Strip lines starting with `_` (comment convention for prompt .md files)
|
|
@@ -123,13 +115,59 @@ function isValidSlug(slug: string): boolean {
|
|
|
123
115
|
return basename(slug) === slug && slug !== "." && slug !== "..";
|
|
124
116
|
}
|
|
125
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Strip LIKE metacharacters so the prefix match runs literally. SQLite has
|
|
120
|
+
* no default LIKE escape character, so strip rather than escape. Inlined
|
|
121
|
+
* from `contacts/contact-store.ts`.
|
|
122
|
+
*/
|
|
123
|
+
function escapeLike(value: string): string {
|
|
124
|
+
return value.replace(/%/g, "").replace(/_/g, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Pure slug transform applied to a display name. */
|
|
128
|
+
function computeUserFileBaseSlug(displayName: string): string {
|
|
129
|
+
return (
|
|
130
|
+
displayName
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
133
|
+
.replace(/^-+|-+$/g, "")
|
|
134
|
+
.slice(0, 100) || "user"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate a collision-free `users/<slug>.md` filename for a display name,
|
|
140
|
+
* inlined verbatim from `contacts/contact-store.ts` to keep this migration
|
|
141
|
+
* self-contained. Produces "alice.md", "alice-2.md", etc.
|
|
142
|
+
*/
|
|
143
|
+
function generateUserFileSlug(db: Database, displayName: string): string {
|
|
144
|
+
const slug = computeUserFileBaseSlug(displayName);
|
|
145
|
+
|
|
146
|
+
const rows = db
|
|
147
|
+
.query<{ user_file: string | null }, [string]>(
|
|
148
|
+
`SELECT user_file FROM contacts WHERE user_file LIKE ?`,
|
|
149
|
+
)
|
|
150
|
+
.all(`${escapeLike(slug)}%`);
|
|
151
|
+
|
|
152
|
+
const taken = new Set(rows.map((r) => r.user_file?.toLowerCase()));
|
|
153
|
+
|
|
154
|
+
const base = `${slug}.md`;
|
|
155
|
+
if (!taken.has(base)) return base;
|
|
156
|
+
|
|
157
|
+
for (let i = 2; ; i++) {
|
|
158
|
+
const candidate = `${slug}-${i}.md`;
|
|
159
|
+
if (!taken.has(candidate)) return candidate;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
/**
|
|
127
164
|
* Delete the legacy `USER.md` at the workspace root after migrating
|
|
128
165
|
* any customized content into `users/<slug>.md`.
|
|
129
166
|
*
|
|
130
|
-
* Handles
|
|
131
|
-
* 1.
|
|
132
|
-
*
|
|
167
|
+
* Handles these relevant states:
|
|
168
|
+
* 1. No local guardian → preserve a customized `USER.md` (the local
|
|
169
|
+
* mirror can be stale, so a customized profile may still belong to a
|
|
170
|
+
* real guardian); delete only the unmodified template/empty file.
|
|
133
171
|
* 2. Pre-017 customized `USER.md`, guardian has no `userFile` →
|
|
134
172
|
* backfill the slug, copy `USER.md` → `users/<slug>.md`, delete `USER.md`.
|
|
135
173
|
* 3. Post-017 state where `users/<slug>.md` already has content →
|
|
@@ -137,6 +175,12 @@ function isValidSlug(slug: string): boolean {
|
|
|
137
175
|
* 4. Missing `users/<slug>.md` after guardian is resolved → seed a bare
|
|
138
176
|
* `GUARDIAN_PERSONA_TEMPLATE` scaffold so downstream readers have a file.
|
|
139
177
|
*/
|
|
178
|
+
interface GuardianRow {
|
|
179
|
+
id: string;
|
|
180
|
+
display_name: string;
|
|
181
|
+
user_file: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
export const dropUserMdMigration: WorkspaceMigration = {
|
|
141
185
|
id: "031-drop-user-md",
|
|
142
186
|
description:
|
|
@@ -145,163 +189,203 @@ export const dropUserMdMigration: WorkspaceMigration = {
|
|
|
145
189
|
run(workspaceDir: string): void {
|
|
146
190
|
const userMdPath = join(workspaceDir, "USER.md");
|
|
147
191
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let
|
|
192
|
+
const dbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
193
|
+
if (!existsSync(dbPath)) return; // DB not created yet — defer cleanup.
|
|
194
|
+
|
|
195
|
+
let db: Database;
|
|
152
196
|
try {
|
|
153
|
-
|
|
154
|
-
if (vellumGuardian) {
|
|
155
|
-
guardian = {
|
|
156
|
-
id: vellumGuardian.contact.id,
|
|
157
|
-
displayName: vellumGuardian.contact.displayName,
|
|
158
|
-
userFile: vellumGuardian.contact.userFile ?? null,
|
|
159
|
-
};
|
|
160
|
-
} else {
|
|
161
|
-
const anyGuardian = listGuardianChannels();
|
|
162
|
-
if (!anyGuardian) {
|
|
163
|
-
// Fresh install or pre-onboarding. If a stale USER.md somehow
|
|
164
|
-
// remains on disk (e.g. leftover from an older build), best-
|
|
165
|
-
// effort remove it so future first runs are clean.
|
|
166
|
-
if (existsSync(userMdPath)) {
|
|
167
|
-
try {
|
|
168
|
-
unlinkSync(userMdPath);
|
|
169
|
-
log.info(
|
|
170
|
-
{ path: userMdPath },
|
|
171
|
-
"Deleted stale pre-onboarding USER.md with no guardian",
|
|
172
|
-
);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
log.warn(
|
|
175
|
-
{ err, path: userMdPath },
|
|
176
|
-
"Failed to delete pre-onboarding USER.md; leaving in place",
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
guardian = {
|
|
183
|
-
id: anyGuardian.contact.id,
|
|
184
|
-
displayName: anyGuardian.contact.displayName,
|
|
185
|
-
userFile: anyGuardian.contact.userFile ?? null,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
197
|
+
db = new Database(dbPath);
|
|
188
198
|
} catch (err) {
|
|
189
|
-
|
|
190
|
-
// startup after DB init will try again.
|
|
191
|
-
log.warn(
|
|
192
|
-
{ err },
|
|
193
|
-
"Failed to resolve guardian contact; deferring USER.md cleanup",
|
|
194
|
-
);
|
|
199
|
+
log.warn({ err }, "Cannot open assistant DB; deferring USER.md cleanup");
|
|
195
200
|
return;
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
try {
|
|
204
|
+
// Resolve the guardian contact from the local DB. Prefer the
|
|
205
|
+
// vellum-channel binding (the canonical native guardian); fall back to
|
|
206
|
+
// whichever guardian has the most recently verified active channel.
|
|
207
|
+
let guardianRow: GuardianRow | null;
|
|
200
208
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
209
|
+
guardianRow =
|
|
210
|
+
db
|
|
211
|
+
.query<GuardianRow, []>(
|
|
212
|
+
`SELECT c.id AS id, c.display_name AS display_name, c.user_file AS user_file
|
|
213
|
+
FROM contacts c JOIN contact_channels cc ON cc.contact_id = c.id
|
|
214
|
+
WHERE c.role = 'guardian' AND cc.status = 'active'
|
|
215
|
+
ORDER BY (cc.type = 'vellum') DESC, cc.verified_at DESC
|
|
216
|
+
LIMIT 1`,
|
|
217
|
+
)
|
|
218
|
+
.get() ?? null;
|
|
212
219
|
} catch (err) {
|
|
220
|
+
// DB not ready or query failed — leave USER.md alone. The next
|
|
221
|
+
// startup after DB init tries again.
|
|
213
222
|
log.warn(
|
|
214
|
-
{ err
|
|
215
|
-
"Failed to
|
|
223
|
+
{ err },
|
|
224
|
+
"Failed to resolve guardian contact; deferring USER.md cleanup",
|
|
216
225
|
);
|
|
217
226
|
return;
|
|
218
227
|
}
|
|
219
|
-
}
|
|
220
228
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
if (!guardianRow) {
|
|
230
|
+
// No local guardian. The mirror can be stale (the gateway mirrors
|
|
231
|
+
// best-effort), so a customized USER.md may still belong to a real
|
|
232
|
+
// guardian — preserve it. Only delete the unmodified template/empty
|
|
233
|
+
// stale file.
|
|
234
|
+
if (existsSync(userMdPath)) {
|
|
235
|
+
let isStaleTemplate = false;
|
|
236
|
+
try {
|
|
237
|
+
isStaleTemplate =
|
|
238
|
+
!statSync(userMdPath).isFile() ||
|
|
239
|
+
isLegacyTemplateContent(readFileSync(userMdPath, "utf-8"));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
log.warn(
|
|
242
|
+
{ err, path: userMdPath },
|
|
243
|
+
"Cannot read USER.md with no guardian; leaving in place",
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isStaleTemplate) {
|
|
249
|
+
log.info(
|
|
250
|
+
{ path: userMdPath },
|
|
251
|
+
"Preserving customized USER.md with no local guardian",
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
229
255
|
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
try {
|
|
257
|
+
unlinkSync(userMdPath);
|
|
258
|
+
log.info(
|
|
259
|
+
{ path: userMdPath },
|
|
260
|
+
"Deleting stale template USER.md with no guardian",
|
|
261
|
+
);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
log.warn(
|
|
264
|
+
{ err, path: userMdPath },
|
|
265
|
+
"Failed to delete stale USER.md; leaving in place",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
232
271
|
|
|
233
|
-
|
|
272
|
+
const guardian = {
|
|
273
|
+
id: guardianRow.id,
|
|
274
|
+
displayName: guardianRow.display_name,
|
|
275
|
+
userFile: guardianRow.user_file ?? null,
|
|
276
|
+
};
|
|
234
277
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
278
|
+
// Backfill a userFile slug on the guardian contact if one isn't set.
|
|
279
|
+
if (!guardian.userFile) {
|
|
280
|
+
try {
|
|
281
|
+
const slug = generateUserFileSlug(db, guardian.displayName);
|
|
282
|
+
db.run("UPDATE contacts SET user_file = ? WHERE id = ?", [
|
|
283
|
+
slug,
|
|
284
|
+
guardian.id,
|
|
285
|
+
]);
|
|
286
|
+
guardian.userFile = slug;
|
|
287
|
+
log.info(
|
|
288
|
+
{ contactId: guardian.id, slug },
|
|
289
|
+
"Backfilled missing guardian.userFile",
|
|
290
|
+
);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
log.warn(
|
|
293
|
+
{ err, contactId: guardian.id },
|
|
294
|
+
"Failed to backfill guardian.userFile; deferring USER.md cleanup",
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
244
297
|
}
|
|
245
|
-
} catch (err) {
|
|
246
|
-
log.warn(
|
|
247
|
-
{ err, path: userMdPath },
|
|
248
|
-
"Failed to read USER.md; treating as unreadable",
|
|
249
|
-
);
|
|
250
298
|
}
|
|
251
|
-
}
|
|
252
299
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// that already populated users/<slug>.md are left untouched.
|
|
256
|
-
if (
|
|
257
|
-
userMdIsCustomized &&
|
|
258
|
-
userMdRaw !== null &&
|
|
259
|
-
destFileIsMissingOrEmpty(destPath)
|
|
260
|
-
) {
|
|
261
|
-
try {
|
|
262
|
-
copyFileSync(userMdPath, destPath);
|
|
263
|
-
log.info(
|
|
264
|
-
{ src: userMdPath, dest: destPath },
|
|
265
|
-
"Copied customized USER.md content into users/<slug>.md",
|
|
266
|
-
);
|
|
267
|
-
} catch (err) {
|
|
300
|
+
const userFile = guardian.userFile;
|
|
301
|
+
if (!userFile || !isValidSlug(userFile)) {
|
|
268
302
|
log.warn(
|
|
269
|
-
{
|
|
270
|
-
"
|
|
303
|
+
{ userFile },
|
|
304
|
+
"Guardian userFile is missing or not a safe basename; deferring USER.md cleanup",
|
|
271
305
|
);
|
|
272
306
|
return;
|
|
273
307
|
}
|
|
274
|
-
}
|
|
275
308
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
309
|
+
const usersDir = join(workspaceDir, "users");
|
|
310
|
+
mkdirSync(usersDir, { recursive: true });
|
|
311
|
+
|
|
312
|
+
const destPath = join(usersDir, userFile);
|
|
313
|
+
|
|
314
|
+
// Read USER.md if it exists and classify its content.
|
|
315
|
+
let userMdRaw: string | null = null;
|
|
316
|
+
let userMdIsCustomized = false;
|
|
317
|
+
if (existsSync(userMdPath)) {
|
|
318
|
+
try {
|
|
319
|
+
// Guard against USER.md being a directory (hostile state).
|
|
320
|
+
if (statSync(userMdPath).isFile()) {
|
|
321
|
+
userMdRaw = readFileSync(userMdPath, "utf-8");
|
|
322
|
+
userMdIsCustomized = !isLegacyTemplateContent(userMdRaw);
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
log.warn(
|
|
326
|
+
{ err, path: userMdPath },
|
|
327
|
+
"Failed to read USER.md; treating as unreadable",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
292
330
|
}
|
|
293
|
-
}
|
|
294
331
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
332
|
+
// Copy customized USER.md content into users/<slug>.md when the
|
|
333
|
+
// destination is missing or effectively empty. Post-017 installs
|
|
334
|
+
// that already populated users/<slug>.md are left untouched.
|
|
335
|
+
if (
|
|
336
|
+
userMdIsCustomized &&
|
|
337
|
+
userMdRaw !== null &&
|
|
338
|
+
destFileIsMissingOrEmpty(destPath)
|
|
339
|
+
) {
|
|
340
|
+
try {
|
|
341
|
+
copyFileSync(userMdPath, destPath);
|
|
342
|
+
log.info(
|
|
343
|
+
{ src: userMdPath, dest: destPath },
|
|
344
|
+
"Copied customized USER.md content into users/<slug>.md",
|
|
345
|
+
);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
log.warn(
|
|
348
|
+
{ err, src: userMdPath, dest: destPath },
|
|
349
|
+
"Failed to copy USER.md; deferring USER.md cleanup",
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Seed the guardian persona scaffold when the destination file
|
|
356
|
+
// still doesn't exist (e.g. no USER.md and no post-017 content).
|
|
357
|
+
// This keeps parity with `ensureGuardianPersonaFile` for new
|
|
358
|
+
// installs so downstream readers always find a file.
|
|
359
|
+
if (!existsSync(destPath)) {
|
|
360
|
+
try {
|
|
361
|
+
writeFileSync(destPath, GUARDIAN_PERSONA_TEMPLATE, "utf-8");
|
|
362
|
+
log.info(
|
|
363
|
+
{ dest: destPath },
|
|
364
|
+
"Seeded guardian persona scaffold at users/<slug>.md",
|
|
365
|
+
);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
log.warn(
|
|
368
|
+
{ err, dest: destPath },
|
|
369
|
+
"Failed to seed guardian persona scaffold; continuing with USER.md deletion",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Finally, delete the legacy USER.md if it still exists — template or
|
|
375
|
+
// customized, it has no remaining consumer.
|
|
376
|
+
if (existsSync(userMdPath)) {
|
|
377
|
+
try {
|
|
378
|
+
unlinkSync(userMdPath);
|
|
379
|
+
log.info({ path: userMdPath }, "Deleted legacy workspace USER.md");
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log.warn(
|
|
382
|
+
{ err, path: userMdPath },
|
|
383
|
+
"Failed to delete legacy USER.md",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
304
386
|
}
|
|
387
|
+
} finally {
|
|
388
|
+
db.close();
|
|
305
389
|
}
|
|
306
390
|
},
|
|
307
391
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strip a persisted `llm.callSites.advisor` entry from existing config files.
|
|
8
|
+
*
|
|
9
|
+
* `advisor` is not a valid `LLMCallSiteEnum` key, so a saved
|
|
10
|
+
* `llm.callSites.advisor.profile` is rejected on parse by the
|
|
11
|
+
* `z.partialRecord(LLMCallSiteEnum, ...)` schema. The loader recovers (logs
|
|
12
|
+
* `Invalid config at "llm.callSites.advisor"...`, deletes the key, re-parses),
|
|
13
|
+
* so it is not a crash — but the warning is logged on every boot, and because
|
|
14
|
+
* `GET /config` serves the raw file the web "Overrides" badge keeps counting
|
|
15
|
+
* the invalid key with no reset path.
|
|
16
|
+
*
|
|
17
|
+
* This migration strips the key once. The now-empty `llm.callSites` object is
|
|
18
|
+
* pruned if `advisor` was its only key (the schema defaults `callSites` to
|
|
19
|
+
* `{}`, so an absent key is equivalent to an empty map). Other call-site keys
|
|
20
|
+
* and the unrelated top-level `llm.advisorProfile` selection are left intact.
|
|
21
|
+
*
|
|
22
|
+
* No-op for configs that never had the key. Idempotent.
|
|
23
|
+
*/
|
|
24
|
+
export const removeAdvisorCallsiteOverrideMigration: WorkspaceMigration = {
|
|
25
|
+
id: "112-remove-advisor-callsite-override",
|
|
26
|
+
description:
|
|
27
|
+
"Remove the stale advisor entry from llm.callSites (advisor call site removed)",
|
|
28
|
+
run(workspaceDir: string): void {
|
|
29
|
+
const configPath = join(workspaceDir, "config.json");
|
|
30
|
+
if (!existsSync(configPath)) return;
|
|
31
|
+
|
|
32
|
+
let config: Record<string, unknown>;
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
35
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
36
|
+
config = raw as Record<string, unknown>;
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const llm = config.llm;
|
|
42
|
+
if (!llm || typeof llm !== "object" || Array.isArray(llm)) return;
|
|
43
|
+
|
|
44
|
+
const callSites = (llm as Record<string, unknown>).callSites;
|
|
45
|
+
if (!callSites || typeof callSites !== "object" || Array.isArray(callSites))
|
|
46
|
+
return;
|
|
47
|
+
|
|
48
|
+
const sites = callSites as Record<string, unknown>;
|
|
49
|
+
if (!("advisor" in sites)) return;
|
|
50
|
+
|
|
51
|
+
delete sites.advisor;
|
|
52
|
+
|
|
53
|
+
// Prune the now-empty callSites map; an absent key is equivalent to the
|
|
54
|
+
// schema's `{}` default.
|
|
55
|
+
if (Object.keys(sites).length === 0) {
|
|
56
|
+
delete (llm as Record<string, unknown>).callSites;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
60
|
+
},
|
|
61
|
+
down(_workspaceDir: string): void {
|
|
62
|
+
// Forward-only — the advisor call site no longer exists.
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -109,6 +109,7 @@ import { dropBalancedEconomyProfileMigration } from "./108-drop-balanced-economy
|
|
|
109
109
|
import { swapQualityProfileToGlm52Migration } from "./109-swap-quality-profile-to-glm-5p2.js";
|
|
110
110
|
import { flipBalancedProfileToTogetherMigration } from "./110-flip-balanced-profile-to-together.js";
|
|
111
111
|
import { pruneSeededCallsiteDefaultsMigration } from "./111-prune-seeded-callsite-defaults.js";
|
|
112
|
+
import { removeAdvisorCallsiteOverrideMigration } from "./112-remove-advisor-callsite-override.js";
|
|
112
113
|
import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
|
|
113
114
|
import type { WorkspaceMigration } from "./types.js";
|
|
114
115
|
|
|
@@ -229,4 +230,5 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
229
230
|
swapQualityProfileToGlm52Migration,
|
|
230
231
|
flipBalancedProfileToTogetherMigration,
|
|
231
232
|
pruneSeededCallsiteDefaultsMigration,
|
|
233
|
+
removeAdvisorCallsiteOverrideMigration,
|
|
232
234
|
];
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// Drive the gate off a controllable llm config + a stubbed default-profile
|
|
4
|
-
// resolver, so we can assert the default-on semantics precisely.
|
|
5
|
-
let mockLlm: {
|
|
6
|
-
profiles: Record<string, { advisorEnabled?: boolean | null }>;
|
|
7
|
-
activeProfile?: string;
|
|
8
|
-
} = { profiles: {} };
|
|
9
|
-
|
|
10
|
-
mock.module("../../../../config/loader.js", () => ({
|
|
11
|
-
getConfig: () => ({ llm: mockLlm }),
|
|
12
|
-
}));
|
|
13
|
-
mock.module("../../../../config/llm-resolver.js", () => ({
|
|
14
|
-
resolveDefaultProfileKey: () => "balanced",
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const { advisorEnabledForProfile } = await import("../advisor-gate.js");
|
|
18
|
-
|
|
19
|
-
describe("advisorEnabledForProfile", () => {
|
|
20
|
-
test("default-on when the profile omits the flag", () => {
|
|
21
|
-
mockLlm = { profiles: { p: {} }, activeProfile: "p" };
|
|
22
|
-
expect(advisorEnabledForProfile("p")).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("default-on when the flag is null", () => {
|
|
26
|
-
mockLlm = { profiles: { p: { advisorEnabled: null } }, activeProfile: "p" };
|
|
27
|
-
expect(advisorEnabledForProfile("p")).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("disabled only on an explicit false", () => {
|
|
31
|
-
mockLlm = {
|
|
32
|
-
profiles: { p: { advisorEnabled: false } },
|
|
33
|
-
activeProfile: "p",
|
|
34
|
-
};
|
|
35
|
-
expect(advisorEnabledForProfile("p")).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("enabled on an explicit true", () => {
|
|
39
|
-
mockLlm = { profiles: { p: { advisorEnabled: true } }, activeProfile: "p" };
|
|
40
|
-
expect(advisorEnabledForProfile("p")).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("falls back to the active profile when modelProfile is null", () => {
|
|
44
|
-
mockLlm = {
|
|
45
|
-
profiles: { a: { advisorEnabled: false } },
|
|
46
|
-
activeProfile: "a",
|
|
47
|
-
};
|
|
48
|
-
expect(advisorEnabledForProfile(null)).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("falls back to the call-site default profile when neither is set", () => {
|
|
52
|
-
// resolveDefaultProfileKey is stubbed to "balanced".
|
|
53
|
-
mockLlm = { profiles: { balanced: { advisorEnabled: false } } };
|
|
54
|
-
expect(advisorEnabledForProfile(null)).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { Message } from "../../../../providers/types.js";
|
|
4
|
-
import {
|
|
5
|
-
getCapture,
|
|
6
|
-
recordMessages,
|
|
7
|
-
recordSystemPrompt,
|
|
8
|
-
resetAdvisorStateForTests,
|
|
9
|
-
seedCapture,
|
|
10
|
-
} from "../advisor-state-store.js";
|
|
11
|
-
|
|
12
|
-
const userMsg = (t: string): Message => ({
|
|
13
|
-
role: "user",
|
|
14
|
-
content: [{ type: "text", text: t }],
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
resetAdvisorStateForTests();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe("advisor state store", () => {
|
|
22
|
-
test("records system prompt and messages independently per conversation", () => {
|
|
23
|
-
recordSystemPrompt("c1", "system A");
|
|
24
|
-
recordMessages("c1", [userMsg("hello")]);
|
|
25
|
-
recordSystemPrompt("c2", "system B");
|
|
26
|
-
|
|
27
|
-
expect(getCapture("c1")?.systemPrompt).toBe("system A");
|
|
28
|
-
expect(getCapture("c1")?.messages).toEqual([userMsg("hello")]);
|
|
29
|
-
expect(getCapture("c2")?.systemPrompt).toBe("system B");
|
|
30
|
-
expect(getCapture("c2")?.messages).toEqual([]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("seedCapture snapshots (copies) the array", () => {
|
|
34
|
-
const live: Message[] = [userMsg("one")];
|
|
35
|
-
seedCapture("c1", live);
|
|
36
|
-
live.push(userMsg("two"));
|
|
37
|
-
expect(getCapture("c1")?.messages).toEqual([userMsg("one")]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("getCapture returns undefined for an unseen conversation", () => {
|
|
41
|
-
expect(getCapture("nope")).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
});
|