@vellumai/assistant 0.6.1 → 0.6.2
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/docker-entrypoint.sh +12 -2
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +1 -1
- package/package.json +1 -1
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/onboarding-template-contract.test.ts +5 -4
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +2 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +0 -29
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +11 -0
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/program.ts +9 -2
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +87 -4
- package/src/config/bundled-skills/gmail/SKILL.md +11 -6
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop.ts +33 -12
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +18 -2
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +64 -7
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +0 -3
- package/src/daemon/conversation.ts +3 -5
- package/src/daemon/handlers/conversations.ts +2 -1
- package/src/daemon/handlers/shared.ts +7 -0
- package/src/daemon/lifecycle.ts +21 -1
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/messages.ts +0 -1
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/server.ts +21 -24
- package/src/daemon/transport-hints.ts +33 -0
- package/src/index.ts +1 -1
- package/src/memory/conversation-crud.ts +15 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -5
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/capability-seed.ts +3 -5
- package/src/memory/group-crud.ts +25 -9
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/prompter.ts +0 -2
- package/src/platform/client.ts +1 -1
- package/src/prompts/templates/BOOTSTRAP.md +14 -5
- package/src/prompts/templates/SOUL.md +11 -11
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +18 -6
- package/src/runtime/routes/conversation-routes.ts +9 -3
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +18 -3
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/types.ts +0 -3
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
ServerMessage,
|
|
42
42
|
UserMessageAttachment,
|
|
43
43
|
} from "./message-protocol.js";
|
|
44
|
+
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
44
45
|
|
|
45
46
|
const log = getLogger("conversation-messaging");
|
|
46
47
|
|
|
@@ -222,6 +223,7 @@ export function enqueueMessage(
|
|
|
222
223
|
metadata?: Record<string, unknown>,
|
|
223
224
|
options?: { isInteractive?: boolean },
|
|
224
225
|
displayContent?: string,
|
|
226
|
+
transport?: ConversationTransportMetadata,
|
|
225
227
|
): { queued: boolean; requestId: string; rejected?: boolean } {
|
|
226
228
|
if (!ctx.processing) {
|
|
227
229
|
return { queued: false, requestId };
|
|
@@ -246,6 +248,7 @@ export function enqueueMessage(
|
|
|
246
248
|
turnChannelContext,
|
|
247
249
|
turnInterfaceContext,
|
|
248
250
|
isInteractive: options?.isInteractive,
|
|
251
|
+
transport,
|
|
249
252
|
displayContent,
|
|
250
253
|
sentAt: Date.now(),
|
|
251
254
|
});
|
|
@@ -15,7 +15,11 @@ import type {
|
|
|
15
15
|
TurnChannelContext,
|
|
16
16
|
TurnInterfaceContext,
|
|
17
17
|
} from "../channels/types.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
parseChannelId,
|
|
20
|
+
parseInterfaceId,
|
|
21
|
+
supportsHostProxy,
|
|
22
|
+
} from "../channels/types.js";
|
|
19
23
|
import { getConfig } from "../config/loader.js";
|
|
20
24
|
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
21
25
|
import { listPendingRequestsByConversationScope } from "../memory/canonical-guardian-store.js";
|
|
@@ -44,6 +48,7 @@ import type {
|
|
|
44
48
|
UserMessageAttachment,
|
|
45
49
|
} from "./message-protocol.js";
|
|
46
50
|
import type { TraceEmitter } from "./trace-emitter.js";
|
|
51
|
+
import { buildTransportHints } from "./transport-hints.js";
|
|
47
52
|
import { resolveVerificationSessionIntent } from "./verification-session-intent.js";
|
|
48
53
|
|
|
49
54
|
const log = getLogger("conversation-process");
|
|
@@ -157,6 +162,8 @@ export interface ProcessConversationContext {
|
|
|
157
162
|
): void;
|
|
158
163
|
/** Force context compaction regardless of threshold/cooldown. */
|
|
159
164
|
forceCompact(): Promise<ContextWindowResult>;
|
|
165
|
+
/** Set transport-derived hints for the conversation. */
|
|
166
|
+
setTransportHints(hints: string[] | undefined): void;
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
function resolveQueuedTurnContext(
|
|
@@ -290,6 +297,15 @@ export async function drainQueue(
|
|
|
290
297
|
conversation.setTurnInterfaceContext(queuedInterfaceCtx);
|
|
291
298
|
}
|
|
292
299
|
|
|
300
|
+
// Apply transport hints from the queued message so each turn uses the
|
|
301
|
+
// transport metadata that arrived with its message. Messages without
|
|
302
|
+
// transport (subagent notifications, surface actions, etc.) inherit the
|
|
303
|
+
// conversation's existing hints — clearing them would erase the user's
|
|
304
|
+
// environment context for internal turns.
|
|
305
|
+
if (next.transport) {
|
|
306
|
+
conversation.setTransportHints(buildTransportHints(next.transport));
|
|
307
|
+
}
|
|
308
|
+
|
|
293
309
|
// Non-interactive queued messages (channel requests) must not execute tools
|
|
294
310
|
// via the desktop host proxy. Clear proxy availability so isAvailable()
|
|
295
311
|
// returns false and tool execution falls back to local.
|
|
@@ -302,7 +318,7 @@ export async function drainQueue(
|
|
|
302
318
|
const interfaceCtx =
|
|
303
319
|
queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
|
|
304
320
|
const sourceInterface = interfaceCtx?.userMessageInterface;
|
|
305
|
-
if (sourceInterface
|
|
321
|
+
if (sourceInterface && supportsHostProxy(sourceInterface)) {
|
|
306
322
|
conversation.restoreProxyAvailability();
|
|
307
323
|
conversation.addPreactivatedSkillId("computer-use");
|
|
308
324
|
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ServerMessage,
|
|
15
15
|
UserMessageAttachment,
|
|
16
16
|
} from "./message-protocol.js";
|
|
17
|
+
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
17
18
|
|
|
18
19
|
const log = getLogger("conversation-queue");
|
|
19
20
|
|
|
@@ -29,6 +30,8 @@ export interface QueuedMessage {
|
|
|
29
30
|
turnInterfaceContext?: TurnInterfaceContext;
|
|
30
31
|
/** When false, the turn has no interactive user and should skip clarification prompts. */
|
|
31
32
|
isInteractive?: boolean;
|
|
33
|
+
/** Transport metadata snapshot captured at enqueue time, applied when this message becomes active. */
|
|
34
|
+
transport?: ConversationTransportMetadata;
|
|
32
35
|
/** Original user message text to persist to DB when recording intent stripping produced a different `content`. */
|
|
33
36
|
displayContent?: string;
|
|
34
37
|
/** Wall-clock time (ms since epoch) when the message was enqueued, used as the display timestamp. */
|
|
@@ -155,6 +158,11 @@ function estimateItemBytes(item: QueuedMessage): number {
|
|
|
155
158
|
bytes += a.data.length * 2;
|
|
156
159
|
if (a.extractedText) bytes += a.extractedText.length * 2;
|
|
157
160
|
}
|
|
161
|
+
// Include transport metadata in the estimate so large transport
|
|
162
|
+
// payloads (e.g. hostHomeDir, hostUsername) count against the budget.
|
|
163
|
+
if (item.transport) {
|
|
164
|
+
bytes += JSON.stringify(item.transport).length * 2;
|
|
165
|
+
}
|
|
158
166
|
// Small fixed overhead for metadata, pointers, etc. (not worth
|
|
159
167
|
// measuring precisely — the content/attachment data dominates).
|
|
160
168
|
bytes += 512;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { type ChannelId, parseInterfaceId } from "../channels/types.js";
|
|
12
12
|
import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
|
|
@@ -533,7 +533,9 @@ export function stripNowScratchpad(messages: Message[]): Message[] {
|
|
|
533
533
|
// PKB (Personal Knowledge Base) injection
|
|
534
534
|
// ---------------------------------------------------------------------------
|
|
535
535
|
|
|
536
|
-
const
|
|
536
|
+
const PKB_DEFAULT_FILES = ["INDEX.md", "essentials.md", "threads.md", "buffer.md"];
|
|
537
|
+
|
|
538
|
+
const AUTOINJECT_FILENAME = "_autoinject.md";
|
|
537
539
|
|
|
538
540
|
/** Max buffer.md lines injected into prompts — keeps context bounded even when filing is off. */
|
|
539
541
|
const MAX_BUFFER_LINES = 50;
|
|
@@ -547,9 +549,35 @@ const PKB_NUDGE =
|
|
|
547
549
|
"Use `remember` for every new fact you learn, immediately, no batching.";
|
|
548
550
|
|
|
549
551
|
/**
|
|
550
|
-
* Read the
|
|
551
|
-
*
|
|
552
|
-
*
|
|
552
|
+
* Read `_autoinject.md` from the PKB directory and return the list of
|
|
553
|
+
* filenames to inject.
|
|
554
|
+
*
|
|
555
|
+
* - Returns `null` when the file is missing or unreadable — callers
|
|
556
|
+
* should fall back to the hardcoded defaults.
|
|
557
|
+
* - Returns `[]` when the file exists but has no entries (empty or
|
|
558
|
+
* comments only) — an explicit opt-out meaning "inject nothing."
|
|
559
|
+
*/
|
|
560
|
+
export function readAutoinjectList(pkbDir: string): string[] | null {
|
|
561
|
+
const filePath = join(pkbDir, AUTOINJECT_FILENAME);
|
|
562
|
+
if (!existsSync(filePath)) return null;
|
|
563
|
+
try {
|
|
564
|
+
const raw = stripCommentLines(readFileSync(filePath, "utf-8"));
|
|
565
|
+
const files = raw
|
|
566
|
+
.split("\n")
|
|
567
|
+
.map((l) => l.trim())
|
|
568
|
+
.filter((l) => l.length > 0);
|
|
569
|
+
return files.length > 0 ? files : [];
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Read the always-loaded PKB files and append a nudge encouraging the
|
|
577
|
+
* assistant to proactively read topic files and use `remember` aggressively.
|
|
578
|
+
*
|
|
579
|
+
* Which files are loaded is determined by `pkb/_autoinject.md` (one filename
|
|
580
|
+
* per line). Falls back to the built-in defaults when that file is absent.
|
|
553
581
|
*
|
|
554
582
|
* Returns the concatenated content ready for injection, or `null` if all
|
|
555
583
|
* files are missing or empty.
|
|
@@ -558,9 +586,14 @@ export function readPkbContext(): string | null {
|
|
|
558
586
|
const pkbDir = join(getWorkspaceDir(), "pkb");
|
|
559
587
|
if (!existsSync(pkbDir)) return null;
|
|
560
588
|
|
|
589
|
+
const filesToInject = readAutoinjectList(pkbDir) ?? PKB_DEFAULT_FILES;
|
|
590
|
+
|
|
561
591
|
const parts: string[] = [];
|
|
562
|
-
for (const file of
|
|
563
|
-
|
|
592
|
+
for (const file of filesToInject) {
|
|
593
|
+
// Path traversal guard: reject entries that escape the pkb directory
|
|
594
|
+
const filePath = resolve(pkbDir, file);
|
|
595
|
+
if (!filePath.startsWith(pkbDir + "/")) continue;
|
|
596
|
+
|
|
564
597
|
if (!existsSync(filePath)) continue;
|
|
565
598
|
try {
|
|
566
599
|
let content = stripCommentLines(readFileSync(filePath, "utf-8")).trim();
|
|
@@ -599,6 +632,7 @@ export function injectPkbContext(message: Message, content: string): Message {
|
|
|
599
632
|
if (
|
|
600
633
|
block.type === "text" &&
|
|
601
634
|
(block.text.startsWith("<memory") ||
|
|
635
|
+
block.text.startsWith("</memory_image>") ||
|
|
602
636
|
block.text.startsWith("<memory_context"))
|
|
603
637
|
) {
|
|
604
638
|
insertIdx = i + 1;
|
|
@@ -1051,6 +1085,7 @@ const RUNTIME_INJECTION_PREFIXES = [
|
|
|
1051
1085
|
"<now_scratchpad>", // backward-compat: strip legacy blocks from pre-rename history
|
|
1052
1086
|
"<pkb>",
|
|
1053
1087
|
"<transport_hints>",
|
|
1088
|
+
"<system_notice>One or more tool calls returned an error.",
|
|
1054
1089
|
];
|
|
1055
1090
|
|
|
1056
1091
|
/**
|
|
@@ -1085,6 +1120,28 @@ export function findLastInjectedNowContent(messages: Message[]): string | null {
|
|
|
1085
1120
|
return null;
|
|
1086
1121
|
}
|
|
1087
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Extract the most recently injected PKB content from the message history.
|
|
1125
|
+
* Returns null if no PKB injection is found.
|
|
1126
|
+
*/
|
|
1127
|
+
export function findLastInjectedPkbContent(
|
|
1128
|
+
messages: Message[],
|
|
1129
|
+
): string | null {
|
|
1130
|
+
const prefix = "<pkb>\n";
|
|
1131
|
+
const suffix = "\n</pkb>";
|
|
1132
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1133
|
+
const msg = messages[i];
|
|
1134
|
+
if (msg.role !== "user") continue;
|
|
1135
|
+
for (const block of msg.content) {
|
|
1136
|
+
if (block.type === "text" && block.text.startsWith(prefix)) {
|
|
1137
|
+
const end = block.text.lastIndexOf(suffix);
|
|
1138
|
+
if (end > prefix.length) return block.text.slice(prefix.length, end);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1088
1145
|
/**
|
|
1089
1146
|
* Controls which runtime injections are applied.
|
|
1090
1147
|
*
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
resolveAppDir,
|
|
10
10
|
updateApp,
|
|
11
11
|
} from "../memory/app-store.js";
|
|
12
|
+
import {
|
|
13
|
+
getMessages,
|
|
14
|
+
updateMessageContent,
|
|
15
|
+
} from "../memory/conversation-crud.js";
|
|
12
16
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
13
17
|
import { getLogger } from "../util/logger.js";
|
|
14
18
|
import { isPlainObject } from "../util/object.js";
|
|
@@ -26,11 +30,69 @@ import type {
|
|
|
26
30
|
UiSurfaceShow,
|
|
27
31
|
} from "./message-protocol.js";
|
|
28
32
|
import { INTERACTIVE_SURFACE_TYPES } from "./message-protocol.js";
|
|
33
|
+
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
29
34
|
import type { UserMessageAttachment } from "./message-types/shared.js";
|
|
30
35
|
|
|
31
36
|
const log = getLogger("conversation-surfaces");
|
|
32
37
|
|
|
33
38
|
const MAX_UNDO_DEPTH = 10;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mark a `ui_surface` content block as completed in the database so that
|
|
42
|
+
* history reconstruction preserves the completion state. Also updates
|
|
43
|
+
* in-memory messages when available.
|
|
44
|
+
*/
|
|
45
|
+
export function markSurfaceCompleted(
|
|
46
|
+
ctx: { conversationId: string; messages?: Array<{ content: unknown }> },
|
|
47
|
+
surfaceId: string,
|
|
48
|
+
summary: string,
|
|
49
|
+
): void {
|
|
50
|
+
// Update in-memory messages when available so subsequent reads within
|
|
51
|
+
// this session see the change without waiting for DB.
|
|
52
|
+
if (ctx.messages) {
|
|
53
|
+
for (let i = ctx.messages.length - 1; i >= 0; i--) {
|
|
54
|
+
const msg = ctx.messages[i];
|
|
55
|
+
if (!Array.isArray(msg.content)) continue;
|
|
56
|
+
for (const block of msg.content) {
|
|
57
|
+
const b = block as Record<string, unknown>;
|
|
58
|
+
if (b.type === "ui_surface" && b.surfaceId === surfaceId) {
|
|
59
|
+
b.completed = true;
|
|
60
|
+
b.completionSummary = summary;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Persist to DB.
|
|
68
|
+
const rows = getMessages(ctx.conversationId);
|
|
69
|
+
for (let r = rows.length - 1; r >= 0; r--) {
|
|
70
|
+
let parsed: unknown[];
|
|
71
|
+
try {
|
|
72
|
+
const result = JSON.parse(rows[r].content);
|
|
73
|
+
if (!Array.isArray(result)) continue;
|
|
74
|
+
parsed = result;
|
|
75
|
+
} catch {
|
|
76
|
+
// Some rows store plain text content (e.g. notification seeding) —
|
|
77
|
+
// skip them and keep scanning.
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let found = false;
|
|
81
|
+
for (const pb of parsed) {
|
|
82
|
+
const rb = pb as Record<string, unknown>;
|
|
83
|
+
if (rb.type === "ui_surface" && rb.surfaceId === surfaceId) {
|
|
84
|
+
rb.completed = true;
|
|
85
|
+
rb.completionSummary = summary;
|
|
86
|
+
found = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (found) {
|
|
91
|
+
updateMessageContent(rows[r].id, JSON.stringify(parsed));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
34
96
|
const TASK_PROGRESS_TEMPLATE_FIELDS = ["title", "status", "steps"] as const;
|
|
35
97
|
|
|
36
98
|
/**
|
|
@@ -226,6 +288,7 @@ export interface SurfaceConversationContext {
|
|
|
226
288
|
metadata?: Record<string, unknown>,
|
|
227
289
|
options?: { isInteractive?: boolean },
|
|
228
290
|
displayContent?: string,
|
|
291
|
+
transport?: ConversationTransportMetadata,
|
|
229
292
|
): { queued: boolean; requestId: string; rejected?: boolean };
|
|
230
293
|
getQueueDepth(): number;
|
|
231
294
|
processMessage(
|
|
@@ -857,6 +920,7 @@ export function handleSurfaceAction(
|
|
|
857
920
|
summary,
|
|
858
921
|
submittedData: mergedData,
|
|
859
922
|
});
|
|
923
|
+
markSurfaceCompleted(ctx, surfaceId, summary);
|
|
860
924
|
}
|
|
861
925
|
|
|
862
926
|
// Extract file attachments from action data so they are sent as proper
|
|
@@ -1442,6 +1506,7 @@ export async function surfaceProxyResolver(
|
|
|
1442
1506
|
summary,
|
|
1443
1507
|
submittedData: lastAction.data,
|
|
1444
1508
|
});
|
|
1509
|
+
markSurfaceCompleted(ctx, surfaceId, summary);
|
|
1445
1510
|
} else {
|
|
1446
1511
|
ctx.sendToClient({
|
|
1447
1512
|
type: "ui_surface_dismiss",
|
|
@@ -89,7 +89,6 @@ export interface ToolSetupContext extends SurfaceConversationContext {
|
|
|
89
89
|
assistantId?: string;
|
|
90
90
|
currentRequestId?: string;
|
|
91
91
|
workingDir: string;
|
|
92
|
-
sandboxOverride?: boolean;
|
|
93
92
|
abortController: AbortController | null;
|
|
94
93
|
/** When set, only tools in this set may execute during the current turn. */
|
|
95
94
|
allowedToolNames?: Set<string>;
|
|
@@ -186,7 +185,6 @@ export function createToolExecutor(
|
|
|
186
185
|
: undefined,
|
|
187
186
|
onOutput,
|
|
188
187
|
signal: ctx.abortController?.signal,
|
|
189
|
-
sandboxOverride: ctx.sandboxOverride,
|
|
190
188
|
allowedToolNames: ctx.allowedToolNames,
|
|
191
189
|
memoryScopeId: ctx.memoryPolicy.scopeId,
|
|
192
190
|
forcePromptSideEffects: ctx.memoryPolicy.strictSideEffects,
|
|
@@ -371,7 +369,6 @@ export function createProxyApprovalCallback(
|
|
|
371
369
|
allowlistOptions,
|
|
372
370
|
scopeOptions,
|
|
373
371
|
undefined,
|
|
374
|
-
undefined,
|
|
375
372
|
ctx.conversationId,
|
|
376
373
|
);
|
|
377
374
|
|
|
@@ -114,6 +114,7 @@ import type {
|
|
|
114
114
|
UsageStats,
|
|
115
115
|
UserMessageAttachment,
|
|
116
116
|
} from "./message-protocol.js";
|
|
117
|
+
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
117
118
|
import type {
|
|
118
119
|
AssistantActivityState,
|
|
119
120
|
ConfirmationStateChanged,
|
|
@@ -156,7 +157,6 @@ export class Conversation {
|
|
|
156
157
|
/** @internal */ sendToClient: (msg: ServerMessage) => void;
|
|
157
158
|
/** @internal */ eventBus = new EventBus<AssistantDomainEvents>();
|
|
158
159
|
/** @internal */ workingDir: string;
|
|
159
|
-
/** @internal */ sandboxOverride?: boolean;
|
|
160
160
|
/** @internal */ allowedToolNames?: Set<string>;
|
|
161
161
|
/** @internal */ toolsDisabledDepth = 0;
|
|
162
162
|
/** @internal */ preactivatedSkillIds?: string[];
|
|
@@ -540,10 +540,6 @@ export class Conversation {
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
setSandboxOverride(enabled: boolean | undefined): void {
|
|
544
|
-
this.sandboxOverride = enabled;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
543
|
setSubagentAllowedTools(tools: Set<string> | undefined): void {
|
|
548
544
|
this.subagentAllowedTools = tools;
|
|
549
545
|
}
|
|
@@ -609,6 +605,7 @@ export class Conversation {
|
|
|
609
605
|
metadata?: Record<string, unknown>,
|
|
610
606
|
options?: { isInteractive?: boolean },
|
|
611
607
|
displayContent?: string,
|
|
608
|
+
transport?: ConversationTransportMetadata,
|
|
612
609
|
): { queued: boolean; requestId: string; rejected?: boolean } {
|
|
613
610
|
return enqueueMessageImpl(
|
|
614
611
|
this,
|
|
@@ -621,6 +618,7 @@ export class Conversation {
|
|
|
621
618
|
metadata,
|
|
622
619
|
options,
|
|
623
620
|
displayContent,
|
|
621
|
+
transport,
|
|
624
622
|
);
|
|
625
623
|
}
|
|
626
624
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type InterfaceId,
|
|
5
5
|
parseChannelId,
|
|
6
6
|
parseInterfaceId,
|
|
7
|
+
supportsHostProxy,
|
|
7
8
|
} from "../../channels/types.js";
|
|
8
9
|
import { getConfig } from "../../config/loader.js";
|
|
9
10
|
import {
|
|
@@ -301,7 +302,7 @@ export async function handleConversationCreate(
|
|
|
301
302
|
// Only create the host bash proxy for desktop client interfaces that can
|
|
302
303
|
// execute commands on the user's machine. Set before updateClient so
|
|
303
304
|
// updateClient's call to hostBashProxy.updateSender targets the new proxy.
|
|
304
|
-
if (transportInterface
|
|
305
|
+
if (supportsHostProxy(transportInterface)) {
|
|
305
306
|
const proxy = new HostBashProxy(sendEvent, (requestId) => {
|
|
306
307
|
pendingInteractions.resolve(requestId);
|
|
307
308
|
});
|
|
@@ -81,6 +81,8 @@ export interface HistorySurface {
|
|
|
81
81
|
data: Record<string, unknown>;
|
|
82
82
|
actions?: Array<{ id: string; label: string; style?: string }>;
|
|
83
83
|
display?: string;
|
|
84
|
+
completed?: boolean;
|
|
85
|
+
completionSummary?: string;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
export interface RenderedHistoryContent {
|
|
@@ -281,6 +283,11 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
281
283
|
: {},
|
|
282
284
|
actions: Array.isArray(block.actions) ? block.actions : undefined,
|
|
283
285
|
display: typeof block.display === "string" ? block.display : undefined,
|
|
286
|
+
completed: block.completed === true ? true : undefined,
|
|
287
|
+
completionSummary:
|
|
288
|
+
typeof block.completionSummary === "string"
|
|
289
|
+
? block.completionSummary
|
|
290
|
+
: undefined,
|
|
284
291
|
};
|
|
285
292
|
surfaces.push(surface);
|
|
286
293
|
contentOrder.push(`surface:${surfaces.length - 1}`);
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { reconcileCallsOnStartup } from "../calls/call-recovery.js";
|
|
|
5
5
|
import { setRelayBroadcast } from "../calls/relay-server.js";
|
|
6
6
|
import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
|
|
7
7
|
import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
|
|
8
|
+
import { initFeatureFlagOverrides } from "../config/assistant-feature-flags.js";
|
|
8
9
|
import {
|
|
9
10
|
getPlatformAssistantId,
|
|
10
11
|
getQdrantHttpPortEnv,
|
|
@@ -269,6 +270,15 @@ export async function runDaemon(): Promise<void> {
|
|
|
269
270
|
const signingKey = resolveSigningKey();
|
|
270
271
|
initAuthSigningKey(signingKey);
|
|
271
272
|
|
|
273
|
+
// Pre-populate the feature flag cache from the gateway so all
|
|
274
|
+
// subsequent sync isAssistantFeatureFlagEnabled() calls have data.
|
|
275
|
+
// Fired non-blocking so a slow or unreachable gateway doesn't delay
|
|
276
|
+
// daemon startup (the fetch has a 10s timeout that would otherwise
|
|
277
|
+
// stall the critical path).
|
|
278
|
+
void initFeatureFlagOverrides().catch((err) =>
|
|
279
|
+
log.warn({ err }, "Background feature flag init failed"),
|
|
280
|
+
);
|
|
281
|
+
|
|
272
282
|
seedInterfaceFiles();
|
|
273
283
|
|
|
274
284
|
log.info("Daemon startup: installing templates and initializing DB");
|
|
@@ -690,7 +700,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
690
700
|
seedUninstalledCatalogSkillMemories,
|
|
691
701
|
} = await import("../memory/graph/capability-seed.js");
|
|
692
702
|
seedSkillGraphNodes();
|
|
693
|
-
seedCliGraphNodes();
|
|
703
|
+
await seedCliGraphNodes();
|
|
694
704
|
void seedUninstalledCatalogSkillMemories().catch((err) =>
|
|
695
705
|
log.warn(
|
|
696
706
|
{ err },
|
|
@@ -763,6 +773,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
763
773
|
},
|
|
764
774
|
routingIntent: schedule.routingIntent,
|
|
765
775
|
routingHints: schedule.routingHints,
|
|
776
|
+
conversationMetadata: {
|
|
777
|
+
groupId: "system:scheduled",
|
|
778
|
+
scheduleJobId: schedule.id,
|
|
779
|
+
source: "schedule",
|
|
780
|
+
},
|
|
766
781
|
dedupeKey: `schedule:notify:${schedule.id}:${Date.now()}`,
|
|
767
782
|
throwOnError: true,
|
|
768
783
|
});
|
|
@@ -782,6 +797,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
782
797
|
scheduleId: schedule.id,
|
|
783
798
|
name: schedule.name,
|
|
784
799
|
},
|
|
800
|
+
conversationMetadata: {
|
|
801
|
+
groupId: "system:scheduled",
|
|
802
|
+
scheduleJobId: schedule.id,
|
|
803
|
+
source: "schedule",
|
|
804
|
+
},
|
|
785
805
|
dedupeKey: `schedule:complete:${schedule.id}:${Date.now()}`,
|
|
786
806
|
});
|
|
787
807
|
},
|
|
@@ -309,6 +309,10 @@ export interface HistoryResponseSurface {
|
|
|
309
309
|
data?: Record<string, unknown>;
|
|
310
310
|
}>;
|
|
311
311
|
display?: string;
|
|
312
|
+
/** True when the surface was completed (e.g. form submitted, action taken). */
|
|
313
|
+
completed?: boolean;
|
|
314
|
+
/** Human-readable summary shown in the completion chip. */
|
|
315
|
+
completionSummary?: string;
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
export interface HistoryResponse {
|
|
@@ -151,7 +151,6 @@ export interface ConfirmationRequest {
|
|
|
151
151
|
newContent: string;
|
|
152
152
|
isNewFile: boolean;
|
|
153
153
|
};
|
|
154
|
-
sandboxed?: boolean;
|
|
155
154
|
conversationId?: string;
|
|
156
155
|
/** When false, the client should hide "always allow" / trust-rule persistence affordances. */
|
|
157
156
|
persistentDecisionsAllowed?: boolean;
|
|
@@ -27,6 +27,18 @@ export interface NotificationConversationCreated {
|
|
|
27
27
|
* and should only be surfaced by clients bound to this guardian identity.
|
|
28
28
|
*/
|
|
29
29
|
targetGuardianPrincipalId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Conversation group identifier propagated from the signal producer.
|
|
32
|
+
* Clients use this to place the conversation in the correct sidebar folder
|
|
33
|
+
* (e.g. "system:scheduled" for schedule completion threads).
|
|
34
|
+
*/
|
|
35
|
+
groupId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Semantic source of the conversation (e.g. "schedule", "reminder").
|
|
38
|
+
* Allows clients to override the default "notification" source so the
|
|
39
|
+
* conversation is attributed correctly.
|
|
40
|
+
*/
|
|
41
|
+
source?: string;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/** Client ack sent after UNUserNotificationCenter.add() completes (or fails). */
|
|
@@ -34,11 +34,21 @@ export interface AvatarUpdated {
|
|
|
34
34
|
avatarPath: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Sent by the daemon when workspace config.json changes on disk. */
|
|
38
|
+
export interface ConfigChanged {
|
|
39
|
+
type: "config_changed";
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/** Sent by the daemon when sounds config or sound files change on disk. */
|
|
38
43
|
export interface SoundsConfigUpdated {
|
|
39
44
|
type: "sounds_config_updated";
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/** Sent by the daemon when feature flag files change on disk. */
|
|
48
|
+
export interface FeatureFlagsChanged {
|
|
49
|
+
type: "feature_flags_changed";
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
/** Response to a generate_avatar request indicating success or failure. */
|
|
43
53
|
export interface GenerateAvatarResponse {
|
|
44
54
|
type: "generate_avatar_response";
|
|
@@ -56,5 +66,7 @@ export type _SettingsClientMessages =
|
|
|
56
66
|
export type _SettingsServerMessages =
|
|
57
67
|
| ClientSettingsUpdate
|
|
58
68
|
| AvatarUpdated
|
|
69
|
+
| ConfigChanged
|
|
59
70
|
| SoundsConfigUpdated
|
|
71
|
+
| FeatureFlagsChanged
|
|
60
72
|
| GenerateAvatarResponse;
|
package/src/daemon/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type InterfaceId,
|
|
18
18
|
parseChannelId,
|
|
19
19
|
parseInterfaceId,
|
|
20
|
+
supportsHostProxy,
|
|
20
21
|
} from "../channels/types.js";
|
|
21
22
|
import { getConfig } from "../config/loader.js";
|
|
22
23
|
import { onContactChange } from "../contacts/contact-events.js";
|
|
@@ -91,6 +92,7 @@ import type {
|
|
|
91
92
|
ServerMessage,
|
|
92
93
|
UserMessageAttachment,
|
|
93
94
|
} from "./message-protocol.js";
|
|
95
|
+
import { buildTransportHints } from "./transport-hints.js";
|
|
94
96
|
|
|
95
97
|
const log = getLogger("server");
|
|
96
98
|
|
|
@@ -373,28 +375,7 @@ export class DaemonServer {
|
|
|
373
375
|
{ channelId: transport.channelId },
|
|
374
376
|
"Transport metadata received",
|
|
375
377
|
);
|
|
376
|
-
|
|
377
|
-
// Build enriched hints: interface ID first, then host environment (macOS
|
|
378
|
-
// only), then any client-provided hints.
|
|
379
|
-
const enrichedHints: string[] = [];
|
|
380
|
-
|
|
381
|
-
const interfaceLabel = parseInterfaceId(transport.interfaceId) ?? "vellum";
|
|
382
|
-
enrichedHints.push(`User is messaging from interface: ${interfaceLabel}`);
|
|
383
|
-
|
|
384
|
-
if (transport.interfaceId === "macos") {
|
|
385
|
-
if (transport.hostHomeDir) {
|
|
386
|
-
enrichedHints.push(`Host home directory: ${transport.hostHomeDir}`);
|
|
387
|
-
}
|
|
388
|
-
if (transport.hostUsername) {
|
|
389
|
-
enrichedHints.push(`Host username: ${transport.hostUsername}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (transport.hints) {
|
|
394
|
-
enrichedHints.push(...transport.hints);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
conversation.setTransportHints(enrichedHints);
|
|
378
|
+
conversation.setTransportHints(buildTransportHints(transport));
|
|
398
379
|
}
|
|
399
380
|
|
|
400
381
|
constructor() {
|
|
@@ -548,10 +529,18 @@ export class DaemonServer {
|
|
|
548
529
|
}
|
|
549
530
|
}
|
|
550
531
|
|
|
532
|
+
private broadcastConfigChanged(): void {
|
|
533
|
+
this.broadcast({ type: "config_changed" });
|
|
534
|
+
}
|
|
535
|
+
|
|
551
536
|
private broadcastSoundsConfigUpdated(): void {
|
|
552
537
|
this.broadcast({ type: "sounds_config_updated" });
|
|
553
538
|
}
|
|
554
539
|
|
|
540
|
+
private broadcastFeatureFlagsChanged(): void {
|
|
541
|
+
this.broadcast({ type: "feature_flags_changed" });
|
|
542
|
+
}
|
|
543
|
+
|
|
555
544
|
private broadcastAvatarUpdated(): void {
|
|
556
545
|
this.broadcast({
|
|
557
546
|
type: "avatar_updated",
|
|
@@ -748,6 +737,8 @@ export class DaemonServer {
|
|
|
748
737
|
() => this.broadcastIdentityChanged(),
|
|
749
738
|
() => this.broadcastSoundsConfigUpdated(),
|
|
750
739
|
() => this.broadcastAvatarUpdated(),
|
|
740
|
+
() => this.broadcastConfigChanged(),
|
|
741
|
+
() => this.broadcastFeatureFlagsChanged(),
|
|
751
742
|
);
|
|
752
743
|
|
|
753
744
|
this.appSourceWatcher.start((appId) => this.handleAppSourceChange(appId));
|
|
@@ -978,7 +969,13 @@ export class DaemonServer {
|
|
|
978
969
|
}
|
|
979
970
|
this.evictor.touch(conversationId);
|
|
980
971
|
} else {
|
|
981
|
-
|
|
972
|
+
// Only apply transport metadata when the conversation is idle.
|
|
973
|
+
// When processing, the hints are stored on the queued message and
|
|
974
|
+
// will be applied at dequeue time — applying them here would
|
|
975
|
+
// overwrite the in-flight conversation's transportHints.
|
|
976
|
+
if (!conversation.isProcessing()) {
|
|
977
|
+
this.applyTransportMetadata(conversation, options);
|
|
978
|
+
}
|
|
982
979
|
this.evictor.touch(conversationId);
|
|
983
980
|
}
|
|
984
981
|
return conversation;
|
|
@@ -1091,7 +1088,7 @@ export class DaemonServer {
|
|
|
1091
1088
|
// Guard: don't replace an active proxy during concurrent turn races —
|
|
1092
1089
|
// another request may have started processing between the isProcessing()
|
|
1093
1090
|
// check above and the await on ensureActorScopedHistory().
|
|
1094
|
-
if (resolvedInterface
|
|
1091
|
+
if (supportsHostProxy(resolvedInterface)) {
|
|
1095
1092
|
if (!conversation.isProcessing() || !conversation.hostBashProxy) {
|
|
1096
1093
|
conversation.setHostBashProxy(
|
|
1097
1094
|
new HostBashProxy(conversation.getCurrentSender(), (requestId) => {
|