@vellumai/assistant 0.5.7 → 0.5.9
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/eslint.config.mjs +0 -31
- 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-runtime-assembly.test.ts +227 -0
- 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-agent-loop.ts +6 -0
- 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-runtime-assembly.ts +61 -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 +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- 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/inline-command-expansions.ts +7 -7
- 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/sensitive-output-placeholders.ts +2 -2
- 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
|
|
|
@@ -83,6 +84,28 @@ export function ensurePromptFiles(): void {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// Seed NOW.md scratchpad — always created if missing, regardless of whether
|
|
88
|
+
// this is a fresh install or not. Kept out of PROMPT_FILES because NOW.md is
|
|
89
|
+
// ephemeral state, not identity context.
|
|
90
|
+
const nowDest = getWorkspacePromptPath("NOW.md");
|
|
91
|
+
if (!existsSync(nowDest)) {
|
|
92
|
+
const nowSrc = join(templatesDir, "NOW.md");
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(nowSrc)) {
|
|
95
|
+
copyFileSync(nowSrc, nowDest);
|
|
96
|
+
log.info(
|
|
97
|
+
{ file: "NOW.md", dest: nowDest },
|
|
98
|
+
"Created NOW.md scratchpad from template",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log.warn(
|
|
103
|
+
{ err, file: "NOW.md" },
|
|
104
|
+
"Failed to create NOW.md from template",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
// Seed users/default.md persona template
|
|
87
110
|
try {
|
|
88
111
|
const usersDir = join(getWorkspaceDir(), "users");
|
|
@@ -119,6 +142,7 @@ export interface BuildSystemPromptOptions {
|
|
|
119
142
|
excludeBootstrap?: boolean;
|
|
120
143
|
userPersona?: string | null;
|
|
121
144
|
channelPersona?: string | null;
|
|
145
|
+
userSlug?: string | null;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
/**
|
|
@@ -219,6 +243,12 @@ export function buildSystemPrompt(options?: BuildSystemPromptOptions): string {
|
|
|
219
243
|
const integrationSection = buildIntegrationSection();
|
|
220
244
|
if (integrationSection) dynamicParts.push(integrationSection);
|
|
221
245
|
|
|
246
|
+
const journalContext = buildJournalContext(
|
|
247
|
+
getConfig().journal?.contextWindowSize ?? 10,
|
|
248
|
+
options?.userSlug,
|
|
249
|
+
);
|
|
250
|
+
if (journalContext) dynamicParts.push(journalContext);
|
|
251
|
+
|
|
222
252
|
const dynamicWithSkills = appendSkillsCatalog(dynamicParts.join("\n\n"));
|
|
223
253
|
|
|
224
254
|
return (
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
_ Lines starting with _ are comments - they won't appear in the system prompt
|
|
2
|
+
_ This is your scratchpad for present-tense state. Overwrite it freely between
|
|
3
|
+
_ turns to capture what's happening right now. Unlike the journal (retrospective,
|
|
4
|
+
_ append-only), this file is ephemeral — a snapshot of your current working state.
|
|
5
|
+
_
|
|
6
|
+
_ # NOW.md
|
|
7
|
+
_
|
|
8
|
+
_ ## Focus
|
|
9
|
+
_
|
|
10
|
+
_ What you're currently working on or paying attention to.
|
|
11
|
+
_
|
|
12
|
+
_ ## Active Threads
|
|
13
|
+
_
|
|
14
|
+
_ Open loops, in-progress tasks, things you're tracking across turns.
|
|
15
|
+
_
|
|
16
|
+
_ ## Context
|
|
17
|
+
_
|
|
18
|
+
_ Key facts, constraints, or situational details relevant to the current session.
|
|
19
|
+
_
|
|
20
|
+
_ ## Upcoming
|
|
21
|
+
_
|
|
22
|
+
_ Near-term things on the horizon — scheduled events, pending actions, deadlines.
|
|
23
|
+
_
|
|
24
|
+
_ ## State
|
|
25
|
+
_
|
|
26
|
+
_ Current priorities, energy, or operational notes. What matters most right now.
|
|
@@ -36,6 +36,26 @@ 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
|
+
|
|
49
|
+
## Scratchpad
|
|
50
|
+
|
|
51
|
+
You have a scratchpad file (`NOW.md`) in your workspace. Unlike your journal (retrospective, append-only), the scratchpad is a single file you overwrite with whatever is relevant right now. It's automatically loaded into your context, so next-you always sees the latest snapshot.
|
|
52
|
+
|
|
53
|
+
**When to update:** Whenever your current state changes — you start a new task, finish one, learn something that affects what you're doing, or the user shifts focus. Don't update on a timer; update when the content is stale.
|
|
54
|
+
|
|
55
|
+
**What goes in:** Current focus and what you're actively working on. Threads you're tracking (waiting on a response, monitoring something, pending follow-ups). Temporary context that matters now but won't matter in a week. Upcoming items and near-term priorities. Anything that helps next-you pick up exactly where you left off.
|
|
56
|
+
|
|
57
|
+
**What stays out:** Anything that belongs in your journal (reflections, narrative entries, things worth remembering long-term). Permanent facts about your user or yourself (those go in memory or your journal). Personality and principles (those live here in SOUL.md).
|
|
58
|
+
|
|
39
59
|
## Vibe
|
|
40
60
|
|
|
41
61
|
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.
|
|
@@ -50,7 +50,6 @@ export function extractContentMarkers(body: string): string[] {
|
|
|
50
50
|
const ids: string[] = [];
|
|
51
51
|
const regex = /<!-- vellum-update-release:(.+?) -->/g;
|
|
52
52
|
let match: RegExpExecArray | null;
|
|
53
|
-
// eslint-disable-next-line no-restricted-syntax -- RegExp.exec returns null
|
|
54
53
|
while ((match = regex.exec(body)) !== null) {
|
|
55
54
|
ids.push(match[1]);
|
|
56
55
|
}
|
|
@@ -62,7 +61,6 @@ export function extractReleaseIds(content: string): string[] {
|
|
|
62
61
|
const ids: string[] = [];
|
|
63
62
|
MARKER_REGEX.lastIndex = 0;
|
|
64
63
|
let match: RegExpExecArray | null;
|
|
65
|
-
// eslint-disable-next-line no-restricted-syntax -- RegExp.exec returns null
|
|
66
64
|
while ((match = MARKER_REGEX.exec(content)) !== null) {
|
|
67
65
|
ids.push(match[1]);
|
|
68
66
|
}
|
|
@@ -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
|
*
|