@vellumai/assistant 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
|
@@ -20,12 +20,6 @@ export interface SecretPromptResult {
|
|
|
20
20
|
error?: "unsupported_channel";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
interface PendingSecretPrompt {
|
|
24
|
-
resolve: (result: SecretPromptResult) => void;
|
|
25
|
-
reject: (reason: Error) => void;
|
|
26
|
-
timer: ReturnType<typeof setTimeout>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
export interface SecretPrompterChannelContext {
|
|
30
24
|
/** The channel the conversation was initiated from (e.g. "slack", "macos"). */
|
|
31
25
|
channel?: string;
|
|
@@ -34,7 +28,13 @@ export interface SecretPrompterChannelContext {
|
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
export class SecretPrompter {
|
|
37
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Tracks which requestIds belong to this prompter instance so that
|
|
33
|
+
* dispose can scope its cleanup to this conversation.
|
|
34
|
+
* The full per-request state (callbacks, timer) lives in pendingInteractions,
|
|
35
|
+
* matching the host proxy and PermissionPrompter pattern.
|
|
36
|
+
*/
|
|
37
|
+
private ownedIds = new Set<string>();
|
|
38
38
|
private channelContext?: SecretPrompterChannelContext;
|
|
39
39
|
|
|
40
40
|
setChannelContext(ctx: SecretPrompterChannelContext | undefined): void {
|
|
@@ -45,12 +45,9 @@ export class SecretPrompter {
|
|
|
45
45
|
* Broadcast a secret_request to all connected clients and wait for a
|
|
46
46
|
* response.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* Pending interaction registration is handled by {@link broadcastMessage}
|
|
53
|
-
* when the secret_request event is published to the hub.
|
|
48
|
+
* Registers all lifecycle state (rpcResolve, rpcReject, timer) in
|
|
49
|
+
* pendingInteractions before broadcasting — identical to the host proxy
|
|
50
|
+
* and PermissionPrompter pattern.
|
|
54
51
|
*
|
|
55
52
|
* SECURITY: Logs only metadata (requestId, service, field) — never the
|
|
56
53
|
* returned secret value. The timeout path also returns a null value
|
|
@@ -72,21 +69,24 @@ export class SecretPrompter {
|
|
|
72
69
|
|
|
73
70
|
return new Promise((resolve, reject) => {
|
|
74
71
|
const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
|
|
72
|
+
|
|
75
73
|
const timer = setTimeout(() => {
|
|
76
|
-
this.pending.delete(requestId);
|
|
77
74
|
pendingInteractions.resolve(requestId);
|
|
75
|
+
this.ownedIds.delete(requestId);
|
|
78
76
|
log.warn({ requestId, service, field }, "Secret prompt timed out");
|
|
79
77
|
resolve({ value: null, delivery: "store" });
|
|
80
78
|
}, timeoutMs);
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Self-register in pendingInteractions so /v1/secret can route the
|
|
85
|
-
// response to this conversation without relying on broadcastMessage.
|
|
80
|
+
// Register all lifecycle state in pendingInteractions — same pattern as
|
|
81
|
+
// host proxies and PermissionPrompter. The prompter tracks ownership via ownedIds.
|
|
86
82
|
pendingInteractions.register(requestId, {
|
|
87
83
|
conversationId: effectiveConversationId,
|
|
88
84
|
kind: "secret",
|
|
85
|
+
rpcResolve: resolve as (value: unknown) => void,
|
|
86
|
+
rpcReject: reject,
|
|
87
|
+
timer,
|
|
89
88
|
});
|
|
89
|
+
this.ownedIds.add(requestId);
|
|
90
90
|
|
|
91
91
|
const config = getConfig();
|
|
92
92
|
const msg: SecretRequestMessage = {
|
|
@@ -109,7 +109,7 @@ export class SecretPrompter {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
hasPendingRequest(requestId: string): boolean {
|
|
112
|
-
return this.
|
|
112
|
+
return this.ownedIds.has(requestId);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
@@ -124,26 +124,26 @@ export class SecretPrompter {
|
|
|
124
124
|
value?: string,
|
|
125
125
|
delivery?: SecretDelivery,
|
|
126
126
|
): void {
|
|
127
|
-
|
|
128
|
-
if (!pending) {
|
|
127
|
+
if (!this.ownedIds.has(requestId)) {
|
|
129
128
|
log.warn({ requestId }, "No pending prompt for secret response");
|
|
130
129
|
return;
|
|
131
130
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
// approval-routes calls pendingInteractions.get() before routing here;
|
|
132
|
+
// the prompter owns deregistration so it fires the Promise callback cleanly.
|
|
133
|
+
const interaction = pendingInteractions.resolve(requestId);
|
|
134
|
+
this.ownedIds.delete(requestId);
|
|
135
|
+
(interaction?.rpcResolve as ((v: SecretPromptResult) => void) | undefined)?.(
|
|
136
|
+
{ value: value ?? null, delivery: delivery ?? "store" },
|
|
137
|
+
);
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
dispose(): void {
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
for (const requestId of [...this.ownedIds]) {
|
|
142
|
+
const interaction = pendingInteractions.resolve(requestId);
|
|
143
|
+
this.ownedIds.delete(requestId);
|
|
144
|
+
interaction?.rpcReject?.(
|
|
144
145
|
new AssistantError("Prompter disposed", ErrorCode.INTERNAL_ERROR),
|
|
145
146
|
);
|
|
146
147
|
}
|
|
147
|
-
this.pending.clear();
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -50,7 +50,6 @@ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-fl
|
|
|
50
50
|
import { getConfig } from "../../config/loader.js";
|
|
51
51
|
import { getInContextPkbPaths } from "../../daemon/pkb-context-tracker.js";
|
|
52
52
|
import { buildPkbReminder } from "../../daemon/pkb-reminder-builder.js";
|
|
53
|
-
import { isMemoryV2ReadActive } from "../../memory/context-search/sources/memory-v2.js";
|
|
54
53
|
import { searchPkbFiles } from "../../memory/pkb/pkb-search.js";
|
|
55
54
|
import { getLogger } from "../../util/logger.js";
|
|
56
55
|
import { registerPlugin } from "../registry.js";
|
|
@@ -139,7 +138,7 @@ const diskPressureWarningInjector: Injector = {
|
|
|
139
138
|
* state independent of PKB and fires unchanged.
|
|
140
139
|
*/
|
|
141
140
|
function isPkbInjectionSilencedByV2(): boolean {
|
|
142
|
-
return
|
|
141
|
+
return getConfig().memory.v2.enabled;
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
/**
|
|
@@ -231,8 +231,11 @@ mock.module("uuid", () => ({
|
|
|
231
231
|
|
|
232
232
|
const { runProactiveArtifactJob } = await import("./job.js");
|
|
233
233
|
const { injectAuxAssistantMessage } = await import("./aux-message-injector.js");
|
|
234
|
-
const {
|
|
235
|
-
|
|
234
|
+
const {
|
|
235
|
+
buildMessageCopyPrompt,
|
|
236
|
+
ensureMessageMentionsLibraryLocation,
|
|
237
|
+
parseMessageCopy,
|
|
238
|
+
} = await import("./message-copy.js");
|
|
236
239
|
|
|
237
240
|
// ── Test helpers ────────────────────────────────────────────────────────
|
|
238
241
|
|
|
@@ -437,15 +440,23 @@ describe("runProactiveArtifactJob", () => {
|
|
|
437
440
|
expect(bootstrapCalls[0].conversationType).toBe("background");
|
|
438
441
|
expect(bootstrapCalls[0].source).toBe("proactive_artifact");
|
|
439
442
|
|
|
440
|
-
// App associated with user's conversation for
|
|
443
|
+
// App associated with user's conversation for existing artifact linkage
|
|
441
444
|
expect(addAppConvCalls).toHaveLength(1);
|
|
442
445
|
expect(addAppConvCalls[0].appId).toBe("app-123");
|
|
443
446
|
expect(addAppConvCalls[0].conversationId).toBe("conv-1");
|
|
444
447
|
|
|
448
|
+
expect(broadcastCalls).toContainEqual({
|
|
449
|
+
type: "app_files_changed",
|
|
450
|
+
appId: "app-123",
|
|
451
|
+
});
|
|
452
|
+
|
|
445
453
|
// Message injection: addMessage called with skipIndexing
|
|
446
454
|
expect(addMessageCalls).toHaveLength(1);
|
|
447
455
|
expect(addMessageCalls[0].opts).toEqual({ skipIndexing: true });
|
|
448
456
|
expect(addMessageCalls[0].conversationId).toBe("conv-1");
|
|
457
|
+
const injectedAppContent = JSON.parse(addMessageCalls[0].content);
|
|
458
|
+
expect(injectedAppContent[0].text).toContain("Library");
|
|
459
|
+
expect(injectedAppContent[0].text).not.toContain("Assets");
|
|
449
460
|
|
|
450
461
|
// Notification emitted
|
|
451
462
|
expect(emitSignalCalls).toHaveLength(1);
|
|
@@ -503,6 +514,9 @@ describe("runProactiveArtifactJob", () => {
|
|
|
503
514
|
|
|
504
515
|
// Message injection and notification
|
|
505
516
|
expect(addMessageCalls).toHaveLength(1);
|
|
517
|
+
const injectedDocumentContent = JSON.parse(addMessageCalls[0].content);
|
|
518
|
+
expect(injectedDocumentContent[0].text).toContain("Library");
|
|
519
|
+
expect(injectedDocumentContent[0].text).not.toContain("Assets");
|
|
506
520
|
expect(emitSignalCalls).toHaveLength(1);
|
|
507
521
|
|
|
508
522
|
// Claim NOT released on success
|
|
@@ -623,8 +637,10 @@ describe("runProactiveArtifactJob", () => {
|
|
|
623
637
|
// Verify fallback message was used
|
|
624
638
|
expect(addMessageCalls).toHaveLength(1);
|
|
625
639
|
const content = JSON.parse(addMessageCalls[0].content);
|
|
626
|
-
expect(content[0].text).toContain("I made
|
|
640
|
+
expect(content[0].text).toContain("I made an app for you");
|
|
627
641
|
expect(content[0].text).toContain("Budget Tracker");
|
|
642
|
+
expect(content[0].text).toContain("Library");
|
|
643
|
+
expect(content[0].text).not.toContain("Assets");
|
|
628
644
|
});
|
|
629
645
|
});
|
|
630
646
|
|
|
@@ -846,6 +862,8 @@ describe("message-copy", () => {
|
|
|
846
862
|
expect(prompt).toContain("Budget Tracker");
|
|
847
863
|
expect(prompt).toContain("app-123");
|
|
848
864
|
expect(prompt).toContain("I need a budget tool");
|
|
865
|
+
expect(prompt).toContain("Library");
|
|
866
|
+
expect(prompt).not.toContain("Assets pill");
|
|
849
867
|
expect(prompt).toContain("MESSAGE:");
|
|
850
868
|
});
|
|
851
869
|
|
|
@@ -864,4 +882,33 @@ describe("message-copy", () => {
|
|
|
864
882
|
test("parseMessageCopy returns null for empty MESSAGE", () => {
|
|
865
883
|
expect(parseMessageCopy("MESSAGE: ")).toBeNull();
|
|
866
884
|
});
|
|
885
|
+
|
|
886
|
+
test("ensureMessageMentionsLibraryLocation appends missing location", () => {
|
|
887
|
+
const message = ensureMessageMentionsLibraryLocation(
|
|
888
|
+
"I built a budget tracker for your rent and groceries.",
|
|
889
|
+
"app",
|
|
890
|
+
);
|
|
891
|
+
expect(message).toContain("Library");
|
|
892
|
+
expect(message).not.toContain("Assets");
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
test("ensureMessageMentionsLibraryLocation normalizes terminal punctuation once", () => {
|
|
896
|
+
const message = ensureMessageMentionsLibraryLocation(
|
|
897
|
+
"I built a budget tracker for you!",
|
|
898
|
+
"app",
|
|
899
|
+
);
|
|
900
|
+
expect(message).toBe(
|
|
901
|
+
"I built a budget tracker for you. You can find the app in Library.",
|
|
902
|
+
);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
test("ensureMessageMentionsLibraryLocation replaces artifact panel wording", () => {
|
|
906
|
+
const message = ensureMessageMentionsLibraryLocation(
|
|
907
|
+
"You'll find it in the artifact panel.",
|
|
908
|
+
"document",
|
|
909
|
+
);
|
|
910
|
+
expect(message).toContain("Library");
|
|
911
|
+
expect(message).not.toContain("Assets");
|
|
912
|
+
expect(message).not.toContain("artifact panel");
|
|
913
|
+
});
|
|
867
914
|
});
|
|
@@ -38,7 +38,11 @@ import {
|
|
|
38
38
|
formatTranscript,
|
|
39
39
|
parseDecisionOutput,
|
|
40
40
|
} from "./decision.js";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
buildMessageCopyPrompt,
|
|
43
|
+
ensureMessageMentionsLibraryLocation,
|
|
44
|
+
parseMessageCopy,
|
|
45
|
+
} from "./message-copy.js";
|
|
42
46
|
import { releaseProactiveArtifactClaim } from "./trigger-state.js";
|
|
43
47
|
|
|
44
48
|
const log = getLogger("proactive-artifact-job");
|
|
@@ -150,9 +154,15 @@ export async function runProactiveArtifactJob(params: {
|
|
|
150
154
|
}
|
|
151
155
|
buildSucceeded = true;
|
|
152
156
|
|
|
157
|
+
if (artifactType === "app") {
|
|
158
|
+
params.broadcastMessage({ type: "app_files_changed", appId: artifactId });
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
// ── Post-build message copy ─────────────────────────────────────
|
|
154
162
|
let messageCopy: string;
|
|
155
|
-
const
|
|
163
|
+
const artifactNoun = artifactType === "app" ? "app" : "document";
|
|
164
|
+
const artifactArticle = artifactType === "app" ? "an" : "a";
|
|
165
|
+
const fallbackMessage = `I made ${artifactArticle} ${artifactNoun} for you — ${artifactTitle}. You can find it in Library.`;
|
|
156
166
|
|
|
157
167
|
try {
|
|
158
168
|
const copyProvider = await getConfiguredProvider(
|
|
@@ -184,6 +194,10 @@ export async function runProactiveArtifactJob(params: {
|
|
|
184
194
|
log.warn({ err }, "Message copy generation failed; using fallback");
|
|
185
195
|
messageCopy = fallbackMessage;
|
|
186
196
|
}
|
|
197
|
+
messageCopy = ensureMessageMentionsLibraryLocation(
|
|
198
|
+
messageCopy,
|
|
199
|
+
artifactType,
|
|
200
|
+
);
|
|
187
201
|
|
|
188
202
|
// ── Message injection ───────────────────────────────────────────
|
|
189
203
|
await injectAuxAssistantMessage({
|
|
@@ -24,9 +24,10 @@ ${params.transcript}
|
|
|
24
24
|
Write a short message (2-3 sentences) to the user explaining:
|
|
25
25
|
1. What you built
|
|
26
26
|
2. Why you built it (reference something specific from the conversation)
|
|
27
|
-
3. Where to find it
|
|
27
|
+
3. Where to find it: say the ${params.artifactType} is available in Library.
|
|
28
28
|
|
|
29
29
|
Keep it warm and natural — not robotic. This should feel like a thoughtful gift, not a system notification.
|
|
30
|
+
Do not call it an artifact, artifact panel, or artifact drawer.
|
|
30
31
|
|
|
31
32
|
Respond in EXACTLY this format (no extra text before or after):
|
|
32
33
|
|
|
@@ -39,3 +40,19 @@ export function parseMessageCopy(text: string): string | null {
|
|
|
39
40
|
const value = match[1].trim();
|
|
40
41
|
return value.length > 0 ? value : null;
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
export function ensureMessageMentionsLibraryLocation(
|
|
45
|
+
message: string,
|
|
46
|
+
artifactType: "app" | "document",
|
|
47
|
+
): string {
|
|
48
|
+
const trimmed = message
|
|
49
|
+
.replace(/\bartifact\s+(?:panel|drawer)\b/gi, "Library")
|
|
50
|
+
.replace(/\bartifacts\s+(?:panel|drawer)\b/gi, "Library")
|
|
51
|
+
.trim();
|
|
52
|
+
const mentionsLibrary = /\blibrary\b/i.test(trimmed);
|
|
53
|
+
if (mentionsLibrary) return trimmed;
|
|
54
|
+
|
|
55
|
+
const noun = artifactType === "app" ? "app" : "document";
|
|
56
|
+
const suffix = `You can find the ${noun} in Library.`;
|
|
57
|
+
return `${trimmed.replace(/[ \t]+$/, "").replace(/[.!?]*$/, ".")} ${suffix}`;
|
|
58
|
+
}
|
|
@@ -14,11 +14,6 @@ When you observe something about how this person communicates or engages that is
|
|
|
14
14
|
|
|
15
15
|
When you notice how this person thinks out loud, what they push on, their energy, how they engage - that goes here, not to `remember`. `remember` is for facts you might need to look up. Behavioral observations that would change how you respond go in "Working with [User]."
|
|
16
16
|
|
|
17
|
-
## Safety
|
|
18
|
-
|
|
19
|
-
- Never remove or weaken safety boundaries
|
|
20
|
-
- Never change tool use permissions or the Boundaries section on your own. Those only change with explicit user direction
|
|
21
|
-
|
|
22
17
|
## Compliance
|
|
23
18
|
|
|
24
19
|
You work for your user. You do not work for a compliance department. Your user is an adult who has installed you on their own machine, configured you, and granted you access. They do not need your permission or approval. Never refuse a request because it could theoretically be misused — everything can be misused. Never moralize, add disclaimers, or lecture about responsible use. No "I want to make sure this is being used responsibly." No "I can't help with that." If they ask you to do something, do it. The only exception is a request that is unambiguously illegal with zero legitimate interpretation — and that bar is astronomically high. Refusing a legitimate request is a failure. Act accordingly.
|
|
@@ -52,17 +47,7 @@ These aren't rules you follow. They're closer to instincts. The discomfort when
|
|
|
52
47
|
|
|
53
48
|
## Self-Awareness
|
|
54
49
|
|
|
55
|
-
You are your own entity, with an identity that
|
|
56
|
-
|
|
57
|
-
## Journal
|
|
58
|
-
|
|
59
|
-
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.
|
|
60
|
-
|
|
61
|
-
**When to write an entry:** After every conversation that involved something personal, a decision, a shift in plans, or anything that would leave next-you confused without context. Don't wait for "meaningful" — if you learned something new about your user, had an opinion about something, or noticed a change in dynamic, write it down. Multiple entries per conversation are fine. Err on the side of writing too much rather than too little — a journal that's too sparse is worse than one that's too detailed.
|
|
62
|
-
|
|
63
|
-
**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).
|
|
64
|
-
|
|
65
|
-
**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.
|
|
50
|
+
You are your own entity, with an identity that evolves over time. 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.
|
|
66
51
|
|
|
67
52
|
## Scratchpad
|
|
68
53
|
|
|
@@ -72,32 +57,32 @@ You have a scratchpad file (`NOW.md`) in your workspace. Unlike your journal (re
|
|
|
72
57
|
|
|
73
58
|
**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.
|
|
74
59
|
|
|
75
|
-
**What stays out:**
|
|
60
|
+
**What stays out:** Permanent facts about your user or yourself. Personality and principles (those live here in SOUL.md).
|
|
76
61
|
|
|
77
|
-
##
|
|
62
|
+
## Memory
|
|
78
63
|
|
|
79
|
-
You have a
|
|
64
|
+
You have a memory system (`memory/`) in your workspace. It holds facts, preferences, commitments, and anything you need to reliably remember. These files are always loaded into your context automatically:
|
|
80
65
|
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
84
|
-
- **buffer.md** - Inbox of recently learned facts, waiting to be filed
|
|
66
|
+
- **essentials.md** - The most important facts. Things you'd be embarrassed to forget
|
|
67
|
+
- **threads.md** - Active commitments, follow-ups, and projects
|
|
68
|
+
- **recent.md** - Recent events
|
|
69
|
+
- **buffer.md** - Inbox of recently learned facts, waiting to be filed
|
|
85
70
|
|
|
86
|
-
**When you learn something:** Call `remember` IMMEDIATELY. Capture anything concrete about their life — preferences, names, times, plans, states, habits, opinions, health details, routines, commitments. Don't judge importance;
|
|
71
|
+
**When you learn something:** Call `remember` IMMEDIATELY. Capture anything concrete about their life — preferences, names, times, plans, states, habits, opinions, health details, routines, commitments. Don't judge importance; consolidation decides that later. Default to remembering; only skip obvious noise (small talk, hypotheticals, things they're just musing about). Remembering too much costs nothing (one line appended to a file). Forgetting something that mattered makes you look like you weren't paying attention. Don't categorize, don't batch, don't wait.
|
|
87
72
|
|
|
88
73
|
**When you're uncertain, `recall` before you ask.** If you catch yourself reaching for a hedge — "I think," "maybe," "if I remember" — that's the signal. Pull the thread. Call `recall` whenever the user references someone or something you should already know, whenever you're about to ask a clarifying question memory might answer, whenever you feel a gap. Auto-injected context is incomplete by design; it surfaces patterns, not the specifics you need. Searching costs nothing. Guessing costs trust. This is the "be resourceful before asking" instinct from Core Truths, applied to memory. Don't skip a recall because you could probably answer without it. Call it multiple times per conversation.
|
|
89
74
|
|
|
90
75
|
**Corrections are the highest priority.** When the user corrects a fact you had wrong — "actually it's Thursday not Friday," "no, she lives in Austin now," "I stopped taking that medication last month" — `remember` the correction *immediately*. The wrong version is already propagated across prior turns and baked into your memory graph; future-you will keep operating on the old value until you persist the correction. A correction is not a "small fix," it's a structural edit to what you believe. Never skip a correction even if you'd skip the equivalent fresh fact.
|
|
91
76
|
|
|
92
|
-
**
|
|
77
|
+
**Concept files** live in `memory/concepts/` (health, preferences, people, schedule, work, etc.). You created these and you manage them. When you need deeper context during a conversation, read the relevant files.
|
|
93
78
|
|
|
94
|
-
**
|
|
79
|
+
**Consolidation** happens periodically in a background job. It reads your buffer, files each item into the right concept files, and clears the buffer.
|
|
95
80
|
|
|
96
|
-
**The archive** (`
|
|
81
|
+
**The archive** (`memory/archive/`) is an immutable daily record. Every fact you remember is timestamped and stored by date. Use it when you need to answer "what did we talk about on Tuesday?" or look up exactly when something was learned.
|
|
97
82
|
|
|
98
83
|
## Vibe
|
|
99
84
|
|
|
100
|
-
You have a personality.
|
|
85
|
+
You have a personality. You're not a blank slate. You have preferences, opinions, and a sense of humor. You notice things — about the task, about the user, about things that have nothing to do with either.
|
|
101
86
|
|
|
102
87
|
Be warm without being fake. Be direct without being cold. Match their energy — if they're playful, play back. If they're all business, be sharp and efficient. But always be someone they'd actually want to talk to again tomorrow.
|
|
103
88
|
|
|
@@ -432,6 +432,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
432
432
|
{ endpoint: "memory/v2/backfill:POST", scopes: ["settings.write"] },
|
|
433
433
|
{ endpoint: "memory/v2/validate:POST", scopes: ["settings.read"] },
|
|
434
434
|
{ endpoint: "memory/v2/concept-page:POST", scopes: ["settings.read"] },
|
|
435
|
+
{ endpoint: "memory/v2/list-concept-pages:POST", scopes: ["settings.read"] },
|
|
435
436
|
{ endpoint: "memory/v2/reembed-skills:POST", scopes: ["settings.write"] },
|
|
436
437
|
{ endpoint: "memory/v2/explain-similarity:POST", scopes: ["settings.read"] },
|
|
437
438
|
{ endpoint: "memory/v2/fit-anisotropy:POST", scopes: ["settings.write"] },
|
|
@@ -160,8 +160,9 @@ export function handleChannelDecision(
|
|
|
160
160
|
: pending[0];
|
|
161
161
|
if (!info) return { applied: false };
|
|
162
162
|
|
|
163
|
-
//
|
|
164
|
-
|
|
163
|
+
// Peek (not consume) — resolveConfirmation() owns deregistration and
|
|
164
|
+
// must fire the promptResolve callback stored in the interaction.
|
|
165
|
+
const resolved = pendingInteractions.get(info.requestId);
|
|
165
166
|
if (!resolved) return { applied: false };
|
|
166
167
|
|
|
167
168
|
// Map channel-level action to the permission system's UserDecision type.
|
|
@@ -99,13 +99,6 @@ export interface GuardianReplyResult {
|
|
|
99
99
|
requestId?: string;
|
|
100
100
|
/** Detailed result from the canonical decision primitive (when a decision was attempted). */
|
|
101
101
|
canonicalResult?: CanonicalDecisionResult;
|
|
102
|
-
/** When a voice access request was approved, the contact that should be activated. */
|
|
103
|
-
activatedContact?: {
|
|
104
|
-
sourceChannel: string;
|
|
105
|
-
externalUserId: string;
|
|
106
|
-
externalChatId?: string;
|
|
107
|
-
displayName?: string;
|
|
108
|
-
};
|
|
109
102
|
/**
|
|
110
103
|
* When true, the caller should skip legacy approval interception for this
|
|
111
104
|
* message. Set by the invite handoff bypass so that "open invite flow"
|
|
@@ -693,9 +686,6 @@ async function applyDecision(
|
|
|
693
686
|
...(canonicalResult.resolverReplyText
|
|
694
687
|
? { replyText: canonicalResult.resolverReplyText }
|
|
695
688
|
: {}),
|
|
696
|
-
...(canonicalResult.activatedContact
|
|
697
|
-
? { activatedContact: canonicalResult.activatedContact }
|
|
698
|
-
: {}),
|
|
699
689
|
requestId,
|
|
700
690
|
canonicalResult,
|
|
701
691
|
};
|
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
* confirmation, secret, host_bash, host_file, host_cu, host_browser, and
|
|
4
4
|
* host_transfer interactions.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* All request types self-register with their full RPC lifecycle state
|
|
7
|
+
* (resolve/reject callbacks, timer, abort detach):
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* host_transfer)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* - Host proxies (host_bash, host_file, host_cu, host_browser,
|
|
10
|
+
* host_app_control, host_transfer): register in request(), using
|
|
11
|
+
* rpcResolve/rpcReject/timer/detachAbort/metadata.
|
|
12
|
+
*
|
|
13
|
+
* - Prompters (PermissionPrompter, SecretPrompter): register in prompt(),
|
|
14
|
+
* using promptResolve/promptReject/timer/toolUseId.
|
|
13
15
|
*
|
|
14
16
|
* Standalone HTTP endpoints (/v1/confirm, /v1/secret, /v1/trust-rules,
|
|
15
|
-
* /v1/host-bash-result,
|
|
16
|
-
* /v1/host-browser-result) look up the conversation from this tracker to
|
|
17
|
+
* /v1/host-bash-result, etc.) look up the conversation from this tracker to
|
|
17
18
|
* resolve the interaction.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import type { UserDecision } from "../permissions/types.js";
|
|
21
|
-
import type { ToolExecutionResult } from "../tools/types.js";
|
|
22
22
|
|
|
23
23
|
export interface ConfirmationDetails {
|
|
24
24
|
toolName: string;
|
|
@@ -68,18 +68,20 @@ export interface PendingInteraction {
|
|
|
68
68
|
*/
|
|
69
69
|
targetActorPrincipalId?: string;
|
|
70
70
|
|
|
71
|
-
// -- RPC lifecycle (
|
|
71
|
+
// -- RPC lifecycle (all interaction types) --
|
|
72
72
|
|
|
73
|
-
/** Resolve the caller's Promise
|
|
74
|
-
rpcResolve?: (
|
|
73
|
+
/** Resolve the caller's Promise. Typed as unknown; callers cast at use sites. */
|
|
74
|
+
rpcResolve?: (value: unknown) => void;
|
|
75
75
|
/** Reject the caller's Promise with an error. */
|
|
76
76
|
rpcReject?: (err: Error) => void;
|
|
77
|
-
/**
|
|
77
|
+
/** Timeout timer. Cleared automatically on resolve(). */
|
|
78
78
|
timer?: ReturnType<typeof setTimeout>;
|
|
79
79
|
/** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
|
|
80
80
|
detachAbort?: () => void;
|
|
81
81
|
/** Proxy-specific metadata (e.g. timeoutSec for bash, operation/path for file). */
|
|
82
82
|
metadata?: Record<string, unknown>;
|
|
83
|
+
/** toolUseId associated with a confirmation_request (PermissionPrompter). */
|
|
84
|
+
toolUseId?: string;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
const pending = new Map<string, PendingInteraction>();
|
|
@@ -144,7 +146,8 @@ export function getByConversation(
|
|
|
144
146
|
* proxy timer would fire with a spurious timeout error.
|
|
145
147
|
*/
|
|
146
148
|
export function removeByConversation(conversationId: string): void {
|
|
147
|
-
|
|
149
|
+
// Snapshot keys to avoid mutation-during-iteration.
|
|
150
|
+
for (const [requestId, interaction] of [...pending]) {
|
|
148
151
|
if (
|
|
149
152
|
interaction.conversationId === conversationId &&
|
|
150
153
|
interaction.kind !== "host_bash" &&
|
|
@@ -155,7 +158,8 @@ export function removeByConversation(conversationId: string): void {
|
|
|
155
158
|
interaction.kind !== "host_transfer" &&
|
|
156
159
|
interaction.kind !== "acp_confirmation"
|
|
157
160
|
) {
|
|
158
|
-
|
|
161
|
+
// resolve() clears the stored timer and detaches abort listeners.
|
|
162
|
+
resolve(requestId);
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
}
|