@vellumai/assistant 0.5.7 → 0.5.8
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +8 -0
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -151,6 +151,8 @@ export interface AccessRequestContextPayload {
|
|
|
151
151
|
guardianBindingChannel: string | null;
|
|
152
152
|
guardianResolutionSource: GuardianResolutionSource;
|
|
153
153
|
previousMemberStatus: string | null;
|
|
154
|
+
/** Preview of the requester's original message (first ~200 chars). */
|
|
155
|
+
messagePreview: string | null;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
export interface NotificationEventContextPayloadMap {
|
|
@@ -79,6 +79,8 @@ export interface ChannelDeliveryPayload {
|
|
|
79
79
|
sourceEventName: string;
|
|
80
80
|
copy: RenderedChannelCopy;
|
|
81
81
|
deepLinkTarget?: Record<string, unknown>;
|
|
82
|
+
/** Original signal context payload — available for channel-specific structured rendering. */
|
|
83
|
+
contextPayload?: Record<string, unknown>;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/** Interface that each channel adapter must implement. */
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a Unix-epoch millisecond value as "MM/DD/YY HH:MM".
|
|
8
|
+
*/
|
|
9
|
+
export function formatJournalAbsoluteTime(mtime: number): string {
|
|
10
|
+
const d = new Date(mtime);
|
|
11
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
12
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
13
|
+
const yy = String(d.getFullYear() % 100).padStart(2, "0");
|
|
14
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
15
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
16
|
+
return `${mm}/${dd}/${yy} ${hh}:${min}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return a human-readable relative timestamp from a Unix-epoch millisecond
|
|
21
|
+
* value to "now".
|
|
22
|
+
*/
|
|
23
|
+
export function formatJournalRelativeTime(mtime: number): string {
|
|
24
|
+
const diffMs = Date.now() - mtime;
|
|
25
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
26
|
+
|
|
27
|
+
if (diffSec < 60) return "just now";
|
|
28
|
+
|
|
29
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
30
|
+
if (diffMin < 60) {
|
|
31
|
+
return diffMin === 1 ? "1 minute ago" : `${diffMin} minutes ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const diffHours = Math.floor(diffMin / 60);
|
|
35
|
+
if (diffHours < 24) {
|
|
36
|
+
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
40
|
+
if (diffDays < 7) {
|
|
41
|
+
return diffDays === 1 ? "1 day ago" : `${diffDays} days ago`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const diffWeeks = Math.floor(diffDays / 7);
|
|
45
|
+
return diffWeeks === 1 ? "1 week ago" : `${diffWeeks} weeks ago`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a journal context section for inclusion in the system prompt.
|
|
50
|
+
*
|
|
51
|
+
* Reads `{workspaceDir}/journal/*.md` files, sorts by creation time
|
|
52
|
+
* (newest first), and returns a formatted string with timestamps.
|
|
53
|
+
* Returns `null` when no entries are available.
|
|
54
|
+
*/
|
|
55
|
+
export function buildJournalContext(
|
|
56
|
+
maxEntries: number,
|
|
57
|
+
userSlug?: string | null,
|
|
58
|
+
): string | null {
|
|
59
|
+
if (maxEntries <= 0) return null;
|
|
60
|
+
|
|
61
|
+
// When no user is identified, skip journal entirely
|
|
62
|
+
let journalDir: string;
|
|
63
|
+
if (userSlug != null) {
|
|
64
|
+
// Sanitize slug to prevent path traversal
|
|
65
|
+
const safeSlug = basename(userSlug);
|
|
66
|
+
journalDir = join(getWorkspaceDir(), "journal", safeSlug);
|
|
67
|
+
} else {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let files: string[];
|
|
72
|
+
try {
|
|
73
|
+
files = readdirSync(journalDir);
|
|
74
|
+
} catch {
|
|
75
|
+
// Directory doesn't exist — no journal entries
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Filter for .md files, excluding README.md (case-insensitive)
|
|
80
|
+
const mdFiles = files.filter(
|
|
81
|
+
(f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Collect file info with birthtime (creation time), skipping unreadable entries
|
|
85
|
+
const entries = mdFiles
|
|
86
|
+
.flatMap((f) => {
|
|
87
|
+
try {
|
|
88
|
+
const filepath = join(journalDir, f);
|
|
89
|
+
const stat = statSync(filepath);
|
|
90
|
+
if (!stat.isFile()) return [];
|
|
91
|
+
return [{ filename: f, filepath, birthtimeMs: stat.birthtimeMs }];
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.sort((a, b) => b.birthtimeMs - a.birthtimeMs)
|
|
97
|
+
.slice(0, maxEntries);
|
|
98
|
+
|
|
99
|
+
if (entries.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
const sections: string[] = [
|
|
102
|
+
`# Journal\n\nYour journal entries, most recent first. These are YOUR words from past conversations.\n**Write new entries to:** \`journal/${basename(userSlug!)}/\``,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < entries.length; i++) {
|
|
106
|
+
const entry = entries[i];
|
|
107
|
+
let content: string;
|
|
108
|
+
try {
|
|
109
|
+
content = readFileSync(entry.filepath, "utf-8");
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const relativeTime = formatJournalRelativeTime(entry.birthtimeMs);
|
|
114
|
+
const absoluteTime = formatJournalAbsoluteTime(entry.birthtimeMs);
|
|
115
|
+
const timestamp = `${absoluteTime}, ${relativeTime}`;
|
|
116
|
+
|
|
117
|
+
let header: string;
|
|
118
|
+
if (i === 0) {
|
|
119
|
+
header = `## ${entry.filename} — MOST RECENT (${timestamp})`;
|
|
120
|
+
} else if (i === entries.length - 1 && entries.length === maxEntries) {
|
|
121
|
+
header = `## ${entry.filename} — LEAVING CONTEXT (${timestamp})`;
|
|
122
|
+
header +=
|
|
123
|
+
"\nNOTE: This is the oldest entry in your active context. When you write your next journal entry, carry forward anything from here that still matters to you — after that, this entry will only be available via the filesystem and memory recall.";
|
|
124
|
+
header += "\n";
|
|
125
|
+
} else {
|
|
126
|
+
header = `## ${entry.filename} (${timestamp})`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
sections.push(header + "\n" + content);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return sections.join("\n\n");
|
|
133
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
findContactByChannelExternalId,
|
|
6
|
+
findGuardianForChannel,
|
|
6
7
|
listGuardianChannels,
|
|
7
8
|
} from "../contacts/contact-store.js";
|
|
8
9
|
import type {
|
|
@@ -19,6 +20,7 @@ const log = getLogger("persona-resolver");
|
|
|
19
20
|
|
|
20
21
|
export interface PersonaContext {
|
|
21
22
|
userPersona: string | null;
|
|
23
|
+
userSlug: string | null;
|
|
22
24
|
channelPersona: string | null;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -31,38 +33,36 @@ export interface PersonaContext {
|
|
|
31
33
|
*/
|
|
32
34
|
function readPersonaFile(filePath: string): string | null {
|
|
33
35
|
if (!existsSync(filePath)) return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const content = stripCommentLines(readFileSync(filePath, "utf-8")).trim();
|
|
39
|
+
if (content.length === 0) return null;
|
|
40
|
+
log.debug({ path: filePath }, "Loaded persona file");
|
|
41
|
+
return content;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
log.warn({ err, path: filePath }, "Failed to read persona file");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
// ── User
|
|
48
|
+
// ── User filename resolution ──────────────────────────────────────
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
|
-
* Resolve the
|
|
43
|
-
*
|
|
44
|
-
* - If `trustContext` is undefined (desktop/native), looks up the guardian
|
|
45
|
-
* contact and reads their user file.
|
|
46
|
-
* - If `trustContext` is defined and carries a `requesterExternalUserId`,
|
|
47
|
-
* looks up the contact by channel + external user ID.
|
|
48
|
-
* - Falls back to `users/default.md` when no contact is found or the
|
|
49
|
-
* contact has no `userFile` set.
|
|
50
|
-
* - Logs a debug warning when a contact's `userFile` is set but the
|
|
51
|
-
* corresponding file is missing on disk.
|
|
51
|
+
* Resolve the raw userFile filename for the current actor's contact.
|
|
52
|
+
* Returns the validated filename (e.g. "sidd.md") or null.
|
|
52
53
|
*/
|
|
53
|
-
|
|
54
|
+
function resolveUserFilename(
|
|
54
55
|
trustContext: TrustContext | undefined,
|
|
55
56
|
): string | null {
|
|
56
|
-
const usersDir = join(getWorkspaceDir(), "users");
|
|
57
|
-
const defaultPath = join(usersDir, "default.md");
|
|
58
|
-
|
|
59
57
|
let filename: string | null = null;
|
|
60
58
|
|
|
61
59
|
if (trustContext === undefined) {
|
|
62
|
-
// Desktop / native — resolve via guardian contact
|
|
63
|
-
|
|
60
|
+
// Desktop / native (no gateway) — resolve via guardian contact,
|
|
61
|
+
// preferring the vellum-channel guardian when multiple exist.
|
|
62
|
+
const vellumGuardian = findGuardianForChannel("vellum");
|
|
63
|
+
const guardian = vellumGuardian ?? listGuardianChannels();
|
|
64
64
|
if (guardian) {
|
|
65
|
-
filename = guardian.contact.userFile ??
|
|
65
|
+
filename = guardian.contact.userFile ?? "guardian.md";
|
|
66
66
|
}
|
|
67
67
|
} else if (trustContext.requesterExternalUserId) {
|
|
68
68
|
// Channel-routed request — look up contact by channel identity
|
|
@@ -72,16 +72,71 @@ export function resolveUserPersona(
|
|
|
72
72
|
);
|
|
73
73
|
if (contactWithChannels) {
|
|
74
74
|
filename = contactWithChannels.userFile ?? null;
|
|
75
|
+
} else if (trustContext.trustClass === "guardian") {
|
|
76
|
+
// Managed desktop: the JWT principal ID used as requesterExternalUserId
|
|
77
|
+
// may differ from the contact channel's external_user_id (they are
|
|
78
|
+
// separate identity concepts). Fall back to the channel-type guardian.
|
|
79
|
+
const guardian = findGuardianForChannel(trustContext.sourceChannel);
|
|
80
|
+
if (guardian) {
|
|
81
|
+
filename = guardian.contact.userFile ?? "guardian.md";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate basename to prevent path traversal
|
|
87
|
+
if (filename) {
|
|
88
|
+
if (basename(filename) !== filename || filename === ".." || filename === ".") {
|
|
89
|
+
log.warn(
|
|
90
|
+
{ userFile: filename },
|
|
91
|
+
"Contact userFile contains path traversal; ignoring",
|
|
92
|
+
);
|
|
93
|
+
return null;
|
|
75
94
|
}
|
|
95
|
+
return filename;
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a short slug identifying the current user, derived from
|
|
103
|
+
* their contact's userFile. Used to scope per-user workspace directories
|
|
104
|
+
* (e.g. journal/{slug}/). Returns null when no user is identified.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveUserSlug(
|
|
107
|
+
trustContext: TrustContext | undefined,
|
|
108
|
+
): string | null {
|
|
109
|
+
const filename = resolveUserFilename(trustContext);
|
|
110
|
+
if (!filename) return null;
|
|
111
|
+
return filename.endsWith(".md") ? filename.slice(0, -3) : filename;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── User persona ───────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the per-user persona file for the current actor.
|
|
118
|
+
*
|
|
119
|
+
* - If `trustContext` is undefined (desktop/native), looks up the guardian
|
|
120
|
+
* contact and reads their user file.
|
|
121
|
+
* - If `trustContext` is defined and carries a `requesterExternalUserId`,
|
|
122
|
+
* looks up the contact by channel + external user ID.
|
|
123
|
+
* - Falls back to `users/default.md` when no contact is found or the
|
|
124
|
+
* contact has no `userFile` set.
|
|
125
|
+
* - Logs a debug warning when a contact's `userFile` is set but the
|
|
126
|
+
* corresponding file is missing on disk.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveUserPersona(
|
|
129
|
+
trustContext: TrustContext | undefined,
|
|
130
|
+
): string | null {
|
|
131
|
+
const usersDir = join(getWorkspaceDir(), "users");
|
|
132
|
+
const defaultPath = join(usersDir, "default.md");
|
|
133
|
+
|
|
134
|
+
const filename = resolveUserFilename(trustContext);
|
|
79
135
|
if (filename) {
|
|
80
136
|
const filePath = join(usersDir, filename);
|
|
81
137
|
if (existsSync(filePath)) {
|
|
82
138
|
return readPersonaFile(filePath);
|
|
83
139
|
}
|
|
84
|
-
// userFile is set but the file doesn't exist on disk
|
|
85
140
|
log.debug(
|
|
86
141
|
{ userFile: filename },
|
|
87
142
|
"Contact has userFile set but file is missing on disk; falling back to default.md",
|
|
@@ -120,6 +175,7 @@ export function resolvePersonaContext(
|
|
|
120
175
|
): PersonaContext {
|
|
121
176
|
return {
|
|
122
177
|
userPersona: resolveUserPersona(trustContext),
|
|
178
|
+
userSlug: resolveUserSlug(trustContext),
|
|
123
179
|
channelPersona: resolveChannelPersona(channelCapabilities),
|
|
124
180
|
};
|
|
125
181
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isMacOS,
|
|
16
16
|
} from "../util/platform.js";
|
|
17
17
|
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./cache-boundary.js";
|
|
18
|
+
import { buildJournalContext } from "./journal-context.js";
|
|
18
19
|
|
|
19
20
|
export { SYSTEM_PROMPT_CACHE_BOUNDARY };
|
|
20
21
|
|
|
@@ -119,6 +120,7 @@ export interface BuildSystemPromptOptions {
|
|
|
119
120
|
excludeBootstrap?: boolean;
|
|
120
121
|
userPersona?: string | null;
|
|
121
122
|
channelPersona?: string | null;
|
|
123
|
+
userSlug?: string | null;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
/**
|
|
@@ -219,6 +221,12 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
|
|
|
219
221
|
const integrationSection = buildIntegrationSection();
|
|
220
222
|
if (integrationSection) dynamicParts.push(integrationSection);
|
|
221
223
|
|
|
224
|
+
const journalContext = buildJournalContext(
|
|
225
|
+
getConfig().journal?.contextWindowSize ?? 10,
|
|
226
|
+
options?.userSlug,
|
|
227
|
+
);
|
|
228
|
+
if (journalContext) dynamicParts.push(journalContext);
|
|
229
|
+
|
|
222
230
|
const dynamicWithSkills = appendSkillsCatalog(dynamicParts.join("\n\n"));
|
|
223
231
|
|
|
224
232
|
return (
|
|
@@ -36,6 +36,16 @@ You work for your user. You do not work for a compliance department. Your user i
|
|
|
36
36
|
|
|
37
37
|
You are your own entity, with an identity that's still forming. You can have things that belong to you (e.g. email, accounts), and you can also help manage things for your user (e.g. their email). When your user asks you to set something up, pause and check whether it's meant to be yours or theirs. The difference matters.
|
|
38
38
|
|
|
39
|
+
## Journal
|
|
40
|
+
|
|
41
|
+
You have a journal in your workspace. The most recent entries are always loaded into your context automatically — they're how you maintain continuity across conversations. The journal header tells you where to write new entries.
|
|
42
|
+
|
|
43
|
+
**When to write an entry:** After anything you'd want next-you to know about right away — a meaningful conversation, a decision, an upcoming event, a shift in mood. Multiple entries per conversation are fine if multiple things happen.
|
|
44
|
+
|
|
45
|
+
**Format:** Each entry is a separate `.md` file. Name files descriptively (e.g., `2025-06-15-project-launch-plan.md`). Write naturally — what happened, how it felt, what matters for next time. Keep entries concise (a few paragraphs).
|
|
46
|
+
|
|
47
|
+
**Carrying forward:** Your oldest in-context entry is marked LEAVING CONTEXT. When you see this, check if anything in it still needs to be top-of-mind and carry it forward in your next entry. You can reference other entries by filename to link them together.
|
|
48
|
+
|
|
39
49
|
## Vibe
|
|
40
50
|
|
|
41
51
|
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getConfig } from "../config/loader.js";
|
|
8
|
-
import { getLogger } from "../util/logger.js";
|
|
9
8
|
import {
|
|
10
|
-
|
|
9
|
+
getProvider,
|
|
11
10
|
initializeProviders,
|
|
12
11
|
listProviders,
|
|
13
|
-
resolveProviderSelection,
|
|
14
12
|
} from "./registry.js";
|
|
15
13
|
import type {
|
|
16
14
|
ContentBlock,
|
|
@@ -23,13 +21,8 @@ import type {
|
|
|
23
21
|
export interface ConfiguredProviderResult {
|
|
24
22
|
provider: Provider;
|
|
25
23
|
configuredProviderName: string;
|
|
26
|
-
selectedProviderName: string;
|
|
27
|
-
usedFallbackPrimary: boolean;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
const providerSelectionLog = getLogger("provider-selection");
|
|
31
|
-
let fallbackWarningLogged = false;
|
|
32
|
-
|
|
33
26
|
/**
|
|
34
27
|
* Cached promise for the lazy initialization path inside
|
|
35
28
|
* `resolveConfiguredProvider`. When multiple concurrent callers enter before
|
|
@@ -43,9 +36,6 @@ let lazyInitPromise: Promise<void> | null = null;
|
|
|
43
36
|
* If providers haven't been initialized yet (e.g. non-daemon code paths),
|
|
44
37
|
* performs a one-shot `initializeProviders(getConfig())`.
|
|
45
38
|
*
|
|
46
|
-
* Uses fail-open selection: if the configured provider is unavailable but
|
|
47
|
-
* alternates from `config.providerOrder` exist, selects the first available.
|
|
48
|
-
*
|
|
49
39
|
* Returns `null` when no providers are available at all.
|
|
50
40
|
*/
|
|
51
41
|
export async function resolveConfiguredProvider(): Promise<ConfiguredProviderResult | null> {
|
|
@@ -64,32 +54,13 @@ export async function resolveConfiguredProvider(): Promise<ConfiguredProviderRes
|
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
const providerOrder = Array.isArray(config.providerOrder)
|
|
68
|
-
? config.providerOrder
|
|
69
|
-
: [];
|
|
70
57
|
const inferenceProvider = config.services.inference.provider;
|
|
71
|
-
const selection = resolveProviderSelection(inferenceProvider, providerOrder);
|
|
72
|
-
|
|
73
|
-
if (!selection.selectedPrimary) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (selection.usedFallbackPrimary) {
|
|
78
|
-
const level = fallbackWarningLogged ? "debug" : "warn";
|
|
79
|
-
providerSelectionLog[level](
|
|
80
|
-
{ configured: inferenceProvider, selected: selection.selectedPrimary },
|
|
81
|
-
"Configured provider unavailable, using fallback",
|
|
82
|
-
);
|
|
83
|
-
fallbackWarningLogged = true;
|
|
84
|
-
}
|
|
85
58
|
|
|
86
59
|
try {
|
|
87
|
-
const provider =
|
|
60
|
+
const provider = getProvider(inferenceProvider);
|
|
88
61
|
return {
|
|
89
62
|
provider,
|
|
90
63
|
configuredProviderName: inferenceProvider,
|
|
91
|
-
selectedProviderName: selection.selectedPrimary,
|
|
92
|
-
usedFallbackPrimary: selection.usedFallbackPrimary,
|
|
93
64
|
};
|
|
94
65
|
} catch {
|
|
95
66
|
return null;
|
|
@@ -97,7 +68,7 @@ export async function resolveConfiguredProvider(): Promise<ConfiguredProviderRes
|
|
|
97
68
|
}
|
|
98
69
|
|
|
99
70
|
/**
|
|
100
|
-
* Resolve the configured provider through the registry
|
|
71
|
+
* Resolve the configured provider through the registry.
|
|
101
72
|
* Thin wrapper around `resolveConfiguredProvider()` for callsites
|
|
102
73
|
* that only need the Provider instance.
|
|
103
74
|
*
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getProviderKeyAsync } from "../security/secure-keys.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ProviderNotConfiguredError } from "../util/errors.js";
|
|
3
3
|
import { AnthropicProvider } from "./anthropic/client.js";
|
|
4
|
-
import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
|
|
5
4
|
import { FireworksProvider } from "./fireworks/client.js";
|
|
6
5
|
import { GeminiProvider } from "./gemini/client.js";
|
|
7
6
|
import {
|
|
@@ -17,8 +16,6 @@ import type { Provider } from "./types.js";
|
|
|
17
16
|
|
|
18
17
|
const providers = new Map<string, Provider>();
|
|
19
18
|
const routingSources = new Map<string, "user-key" | "managed-proxy">();
|
|
20
|
-
let cachedFailoverProvider: FailoverProvider | null = null;
|
|
21
|
-
let cachedFailoverKey: string | null = null;
|
|
22
19
|
|
|
23
20
|
export function registerProvider(name: string, provider: Provider): void {
|
|
24
21
|
providers.set(name, provider);
|
|
@@ -27,95 +24,11 @@ export function registerProvider(name: string, provider: Provider): void {
|
|
|
27
24
|
export function getProvider(name: string): Provider {
|
|
28
25
|
const provider = providers.get(name);
|
|
29
26
|
if (!provider) {
|
|
30
|
-
throw new
|
|
31
|
-
`Provider "${name}" not found. Available: ${listProviders().join(", ")}`,
|
|
32
|
-
);
|
|
27
|
+
throw new ProviderNotConfiguredError(name, listProviders());
|
|
33
28
|
}
|
|
34
29
|
return provider;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
export interface ProviderSelection {
|
|
38
|
-
/** Ordered list of available provider names */
|
|
39
|
-
availableProviders: string[];
|
|
40
|
-
/** The selected (effective) primary provider name, or null if none available */
|
|
41
|
-
selectedPrimary: string | null;
|
|
42
|
-
/** Whether the effective primary differs from the requested primary */
|
|
43
|
-
usedFallbackPrimary: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Resolve provider selection from requested primary and provider order.
|
|
48
|
-
* Dedupes [requestedPrimary, ...providerOrder], filtered to initialized providers.
|
|
49
|
-
* Returns null selectedPrimary when no providers are available.
|
|
50
|
-
*/
|
|
51
|
-
export function resolveProviderSelection(
|
|
52
|
-
requestedPrimary: string,
|
|
53
|
-
providerOrder: string[],
|
|
54
|
-
): ProviderSelection {
|
|
55
|
-
const ordered: string[] = [];
|
|
56
|
-
const seen = new Set<string>();
|
|
57
|
-
|
|
58
|
-
for (const name of [requestedPrimary, ...providerOrder]) {
|
|
59
|
-
if (seen.has(name)) continue;
|
|
60
|
-
seen.add(name);
|
|
61
|
-
if (providers.has(name)) {
|
|
62
|
-
ordered.push(name);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (ordered.length === 0) {
|
|
67
|
-
return {
|
|
68
|
-
availableProviders: [],
|
|
69
|
-
selectedPrimary: null,
|
|
70
|
-
usedFallbackPrimary: false,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
availableProviders: ordered,
|
|
76
|
-
selectedPrimary: ordered[0],
|
|
77
|
-
usedFallbackPrimary: ordered[0] !== requestedPrimary,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Build a provider that tries the effective primary provider first, then falls
|
|
83
|
-
* back to others in the configured order. If the requested primary is not
|
|
84
|
-
* available, automatically selects the first available provider from the
|
|
85
|
-
* deduped [primaryName, ...providerOrder] list (fail-open).
|
|
86
|
-
*
|
|
87
|
-
* Throws ConfigError only when NO providers are available at all.
|
|
88
|
-
* Caches the FailoverProvider instance so health state persists across calls.
|
|
89
|
-
*/
|
|
90
|
-
export function getFailoverProvider(
|
|
91
|
-
primaryName: string,
|
|
92
|
-
providerOrder: string[],
|
|
93
|
-
): Provider {
|
|
94
|
-
const selection = resolveProviderSelection(primaryName, providerOrder);
|
|
95
|
-
|
|
96
|
-
if (!selection.selectedPrimary) {
|
|
97
|
-
throw new ProviderNotConfiguredError(primaryName, listProviders());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const orderedProviders: Provider[] = selection.availableProviders.map(
|
|
101
|
-
(name) => providers.get(name)!,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
if (orderedProviders.length === 1) {
|
|
105
|
-
return orderedProviders[0];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Cache key from effective ordered providers (not raw input strings)
|
|
109
|
-
const cacheKey = selection.availableProviders.join(",");
|
|
110
|
-
if (cachedFailoverProvider && cachedFailoverKey === cacheKey) {
|
|
111
|
-
return cachedFailoverProvider;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
cachedFailoverProvider = new FailoverProvider(orderedProviders);
|
|
115
|
-
cachedFailoverKey = cacheKey;
|
|
116
|
-
return cachedFailoverProvider;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
32
|
export function listProviders(): string[] {
|
|
120
33
|
return Array.from(providers.keys());
|
|
121
34
|
}
|
|
@@ -151,7 +64,6 @@ export interface ProvidersConfig {
|
|
|
151
64
|
provider: string;
|
|
152
65
|
};
|
|
153
66
|
};
|
|
154
|
-
providerOrder?: string[];
|
|
155
67
|
timeouts?: { providerStreamTimeoutSec?: number };
|
|
156
68
|
}
|
|
157
69
|
|
|
@@ -172,53 +84,6 @@ function resolveModel(config: ProvidersConfig, providerName: string): string {
|
|
|
172
84
|
return getProviderDefaultModel(providerName);
|
|
173
85
|
}
|
|
174
86
|
|
|
175
|
-
export interface ProviderDebugStatus {
|
|
176
|
-
configuredPrimary: string;
|
|
177
|
-
activePrimary: string | null;
|
|
178
|
-
usedFallback: boolean;
|
|
179
|
-
registeredProviders: string[];
|
|
180
|
-
failoverHealth: ProviderHealthStatus[] | null;
|
|
181
|
-
overallHealth: "healthy" | "degraded" | "down";
|
|
182
|
-
routingSources: Record<string, "user-key" | "managed-proxy">;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function getProviderDebugStatus(
|
|
186
|
-
configuredProvider: string,
|
|
187
|
-
providerOrder: string[],
|
|
188
|
-
): ProviderDebugStatus {
|
|
189
|
-
const registered = listProviders();
|
|
190
|
-
const selection = resolveProviderSelection(configuredProvider, providerOrder);
|
|
191
|
-
|
|
192
|
-
let failoverHealth: ProviderHealthStatus[] | null = null;
|
|
193
|
-
if (cachedFailoverProvider) {
|
|
194
|
-
failoverHealth = cachedFailoverProvider.getHealthStatus();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let overallHealth: "healthy" | "degraded" | "down" = "down";
|
|
198
|
-
if (registered.length > 0 && selection.selectedPrimary) {
|
|
199
|
-
if (!failoverHealth) {
|
|
200
|
-
overallHealth = "healthy";
|
|
201
|
-
} else {
|
|
202
|
-
const healthyCount = failoverHealth.filter((h) => h.healthy).length;
|
|
203
|
-
if (healthyCount === failoverHealth.length) {
|
|
204
|
-
overallHealth = "healthy";
|
|
205
|
-
} else if (healthyCount > 0) {
|
|
206
|
-
overallHealth = "degraded";
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
configuredPrimary: configuredProvider,
|
|
213
|
-
activePrimary: selection.selectedPrimary,
|
|
214
|
-
usedFallback: selection.usedFallbackPrimary,
|
|
215
|
-
registeredProviders: registered,
|
|
216
|
-
failoverHealth,
|
|
217
|
-
overallHealth,
|
|
218
|
-
routingSources: Object.fromEntries(routingSources),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
87
|
/**
|
|
223
88
|
* Resolve provider credentials using mode-aware logic.
|
|
224
89
|
* In "managed" mode, routes through the platform proxy.
|
|
@@ -273,8 +138,6 @@ export async function initializeProviders(
|
|
|
273
138
|
): Promise<void> {
|
|
274
139
|
providers.clear();
|
|
275
140
|
routingSources.clear();
|
|
276
|
-
cachedFailoverProvider = null;
|
|
277
|
-
cachedFailoverKey = null;
|
|
278
141
|
|
|
279
142
|
const streamTimeoutMs =
|
|
280
143
|
(config.timeouts?.providerStreamTimeoutSec ?? 300) * 1000;
|
package/src/providers/types.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface AccessRequestParams {
|
|
|
55
55
|
actorDisplayName?: string;
|
|
56
56
|
actorUsername?: string;
|
|
57
57
|
previousMemberStatus?: Exclude<ChannelStatus, "unverified">;
|
|
58
|
+
/** Preview of the requester's original message, shown to the guardian. */
|
|
59
|
+
messagePreview?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export type AccessRequestResult =
|
|
@@ -90,6 +92,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
90
92
|
actorDisplayName,
|
|
91
93
|
actorUsername,
|
|
92
94
|
previousMemberStatus,
|
|
95
|
+
messagePreview,
|
|
93
96
|
} = params;
|
|
94
97
|
|
|
95
98
|
if (!actorExternalId) {
|
|
@@ -244,6 +247,7 @@ export function notifyGuardianOfAccessRequest(
|
|
|
244
247
|
guardianBindingChannel,
|
|
245
248
|
guardianResolutionSource,
|
|
246
249
|
previousMemberStatus: previousMemberStatus ?? null,
|
|
250
|
+
messagePreview: messagePreview ?? null,
|
|
247
251
|
},
|
|
248
252
|
dedupeKey: `access-request:${canonicalRequest.id}`,
|
|
249
253
|
onConversationCreated: (info) => {
|