@vellumai/assistant 0.10.3 → 0.10.4-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -6,50 +6,33 @@
|
|
|
6
6
|
* identity and access-control state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { emitContactChange } from "./contact-events.js";
|
|
10
9
|
import {
|
|
11
10
|
findContactChannel,
|
|
12
|
-
findGuardianForChannel,
|
|
13
11
|
getChannelById,
|
|
14
12
|
getContact,
|
|
15
13
|
getContactInternal,
|
|
16
|
-
updateChannelStatus,
|
|
17
14
|
upsertContact,
|
|
18
15
|
} from "./contact-store.js";
|
|
19
|
-
import type {
|
|
20
|
-
ChannelPolicy,
|
|
21
|
-
ChannelStatus,
|
|
22
|
-
ContactRole,
|
|
23
|
-
ContactWriteResult,
|
|
24
|
-
} from "./types.js";
|
|
16
|
+
import type { ContactWriteResult } from "./types.js";
|
|
25
17
|
|
|
26
18
|
// ── Guardian operations ──────────────────────────────────────────────
|
|
27
19
|
|
|
28
20
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
21
|
+
* No-op shim: the guardian channel ACL revoke is gateway-owned (relayed via
|
|
22
|
+
* mark_channel_revoked). Retained while callers still invoke it; the return is
|
|
23
|
+
* discarded.
|
|
31
24
|
*/
|
|
32
|
-
export function revokeGuardianBinding(
|
|
33
|
-
|
|
34
|
-
// updateChannelStatus write below mutates, so it must stay transactionally
|
|
35
|
-
// consistent with that write. Leave for Combo 11 / gateway-bootstrap-binding.
|
|
36
|
-
const guardian = findGuardianForChannel(channel);
|
|
37
|
-
if (!guardian) return false;
|
|
38
|
-
|
|
39
|
-
updateChannelStatus(guardian.channel.id, {
|
|
40
|
-
status: "revoked",
|
|
41
|
-
revokedReason: "binding_revoked",
|
|
42
|
-
});
|
|
43
|
-
emitContactChange();
|
|
44
|
-
return true;
|
|
25
|
+
export function revokeGuardianBinding(_channel: string): boolean {
|
|
26
|
+
return false;
|
|
45
27
|
}
|
|
46
28
|
|
|
47
29
|
// ── Member operations ────────────────────────────────────────────────
|
|
48
30
|
|
|
49
31
|
/**
|
|
50
|
-
* Upsert a contact and channel by writing to the contacts table.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
32
|
+
* Upsert a contact and channel identity by writing to the contacts table.
|
|
33
|
+
* Persists only identity/INFO — the gateway owns the ACL verdict. Returns the
|
|
34
|
+
* native Contact + ContactChannel, or null if no usable identity was provided
|
|
35
|
+
* or the lookup failed after upsert.
|
|
53
36
|
*/
|
|
54
37
|
export function upsertContactChannel(params: {
|
|
55
38
|
sourceChannel: string;
|
|
@@ -57,12 +40,7 @@ export function upsertContactChannel(params: {
|
|
|
57
40
|
externalChatId?: string;
|
|
58
41
|
displayName?: string;
|
|
59
42
|
username?: string;
|
|
60
|
-
policy?: string;
|
|
61
|
-
status?: string;
|
|
62
43
|
inviteId?: string;
|
|
63
|
-
verifiedAt?: number;
|
|
64
|
-
verifiedVia?: string;
|
|
65
|
-
role?: ContactRole;
|
|
66
44
|
contactId?: string;
|
|
67
45
|
}): ContactWriteResult | null {
|
|
68
46
|
let address: string;
|
|
@@ -91,19 +69,12 @@ export function upsertContactChannel(params: {
|
|
|
91
69
|
upsertContact({
|
|
92
70
|
id: params.contactId,
|
|
93
71
|
displayName,
|
|
94
|
-
role: params.role,
|
|
95
72
|
channels: [
|
|
96
73
|
{
|
|
97
74
|
type: params.sourceChannel,
|
|
98
75
|
address,
|
|
99
76
|
externalChatId: params.externalChatId ?? null,
|
|
100
|
-
status: (params.status as ChannelStatus) ?? undefined,
|
|
101
|
-
policy: (params.policy as ChannelPolicy) ?? undefined,
|
|
102
77
|
inviteId: params.inviteId ?? null,
|
|
103
|
-
revokedReason: params.status === "active" ? null : undefined,
|
|
104
|
-
blockedReason: params.status === "active" ? null : undefined,
|
|
105
|
-
verifiedAt: params.verifiedAt ?? undefined,
|
|
106
|
-
verifiedVia: params.verifiedVia ?? undefined,
|
|
107
78
|
},
|
|
108
79
|
],
|
|
109
80
|
// When a specific contactId is provided, reassign conflicting channels from
|
|
@@ -132,32 +103,21 @@ export function upsertContactChannel(params: {
|
|
|
132
103
|
}
|
|
133
104
|
|
|
134
105
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
106
|
+
* Resolve the native contact/channel for a member id. The ACL downgrade is
|
|
107
|
+
* gateway-owned (relayed via mark_channel_revoked); this no longer mutates
|
|
108
|
+
* local status. The memberId may be a plain channel ID (internal callers) or a
|
|
109
|
+
* composite contactId:channelId (from the API response format).
|
|
138
110
|
*/
|
|
139
|
-
export function revokeMember(
|
|
140
|
-
memberId: string,
|
|
141
|
-
reason?: string,
|
|
142
|
-
): ContactWriteResult | null {
|
|
111
|
+
export function revokeMember(memberId: string): ContactWriteResult | null {
|
|
143
112
|
const channelId = memberId.includes(":") ? memberId.split(":")[1] : memberId;
|
|
144
113
|
|
|
145
114
|
const channelRow = getChannelById(channelId);
|
|
146
115
|
if (!channelRow) return null;
|
|
147
|
-
if (channelRow.status !== "active" && channelRow.status !== "pending")
|
|
148
|
-
return null;
|
|
149
|
-
|
|
150
|
-
updateChannelStatus(channelId, {
|
|
151
|
-
status: "revoked",
|
|
152
|
-
revokedReason: reason ?? null,
|
|
153
|
-
});
|
|
154
116
|
|
|
155
|
-
// Use unscoped lookup — the contact was already resolved via channel ID
|
|
156
117
|
const contact = getContactInternal(channelRow.contactId);
|
|
157
118
|
if (!contact) return null;
|
|
158
|
-
const
|
|
159
|
-
if (!
|
|
119
|
+
const channel = contact.channels.find((ch) => ch.id === channelId);
|
|
120
|
+
if (!channel) return null;
|
|
160
121
|
|
|
161
|
-
|
|
162
|
-
return { contact, channel: updatedChannel };
|
|
122
|
+
return { contact, channel };
|
|
163
123
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { GetContactIpcResponseSchema } from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
2
|
+
|
|
3
|
+
import { ipcCallPersistent } from "../ipc/gateway-client.js";
|
|
4
|
+
import { getLogger } from "../util/logger.js";
|
|
5
|
+
import type { ContactChannel } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const log = getLogger("gateway-channel-read");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read a contact channel's verified state from the gateway contact-channel read
|
|
11
|
+
* (ACL source of truth). Covers all contacts, not just guardian deliveries.
|
|
12
|
+
*
|
|
13
|
+
* Matches the gateway row by logical identity — `(type, address)`,
|
|
14
|
+
* case-insensitive — not by id, so a reconcile-divergent row that the gateway
|
|
15
|
+
* write helpers re-keyed under a different UUID is still found.
|
|
16
|
+
*
|
|
17
|
+
* Returns `undefined` for unreachable reads (gateway down, IPC timeout, schema
|
|
18
|
+
* mismatch) or when no such channel exists, so callers fail open.
|
|
19
|
+
*/
|
|
20
|
+
export async function gatewayContactChannelState(
|
|
21
|
+
channel: Pick<ContactChannel, "contactId" | "type" | "address">,
|
|
22
|
+
): Promise<{ status: string; verifiedAt: number | null } | undefined> {
|
|
23
|
+
let result: unknown;
|
|
24
|
+
try {
|
|
25
|
+
result = await ipcCallPersistent("contacts_get_rich", {
|
|
26
|
+
contactId: channel.contactId,
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
log.warn(
|
|
30
|
+
{ err, contactId: channel.contactId },
|
|
31
|
+
"contacts_get_rich unreachable — failing open",
|
|
32
|
+
);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!result || (result as { contact?: unknown }).contact == null) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const parsed = GetContactIpcResponseSchema.safeParse(result);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
log.warn(
|
|
41
|
+
{ err: parsed.error, contactId: channel.contactId },
|
|
42
|
+
"contacts_get_rich response failed schema parse — failing open",
|
|
43
|
+
);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const address = channel.address.toLowerCase();
|
|
47
|
+
const ch = parsed.data.contact.channels.find(
|
|
48
|
+
(c) => c.type === channel.type && c.address.toLowerCase() === address,
|
|
49
|
+
);
|
|
50
|
+
return ch ? { status: ch.status, verifiedAt: ch.verifiedAt } : undefined;
|
|
51
|
+
}
|
|
@@ -47,13 +47,14 @@ export type ActivateMemberOutcome =
|
|
|
47
47
|
* assistant DB best-effort. The gateway owns the ACL outcome; the local mirror
|
|
48
48
|
* supplies the native contact/channel callers still wire downstream.
|
|
49
49
|
*
|
|
50
|
-
* A gateway failure fails
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* A gateway write failure fails closed: the mirror is identity-only, so a
|
|
51
|
+
* gateway that did not persist the activation must surface as `refused` rather
|
|
52
|
+
* than reporting success off a local row the gateway never verified.
|
|
53
53
|
*
|
|
54
54
|
* Returns an `activated` outcome with a stable memberId on success, or
|
|
55
|
-
* `refused` when the gateway denies the actor. A
|
|
56
|
-
* failure never downgrades a verified gateway
|
|
55
|
+
* `refused` when the gateway denies the actor or the gateway write fails. A
|
|
56
|
+
* best-effort local-mirror failure never downgrades a verified gateway
|
|
57
|
+
* activation.
|
|
57
58
|
*/
|
|
58
59
|
export async function activateMemberChannel(
|
|
59
60
|
params: ActivateMemberChannelParams,
|
|
@@ -89,12 +90,14 @@ export async function activateMemberChannel(
|
|
|
89
90
|
}
|
|
90
91
|
gatewayChannelId = parsed.channel?.id;
|
|
91
92
|
} catch (err) {
|
|
92
|
-
// Fail-
|
|
93
|
-
//
|
|
93
|
+
// Fail-closed: the gateway owns the ACL verdict and the local mirror is
|
|
94
|
+
// identity-only. If the gateway write did not land, the activation is not
|
|
95
|
+
// persisted to the source of truth, so we must not report success.
|
|
94
96
|
log.warn(
|
|
95
97
|
{ err, sourceChannel: params.sourceChannel },
|
|
96
|
-
"upsert_verified_channel relay
|
|
98
|
+
"upsert_verified_channel relay failed — refusing activation (gateway write did not land)",
|
|
97
99
|
);
|
|
100
|
+
return { status: "refused" };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -113,7 +116,10 @@ export async function activateMemberChannel(
|
|
|
113
116
|
return { status: "activated", memberId, member };
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Best-effort local mirror of the activation. Swallows failures. Persists only
|
|
121
|
+
* the native contact/channel identity row — the gateway owns the ACL verdict.
|
|
122
|
+
*/
|
|
117
123
|
function mirrorLocalActivation(
|
|
118
124
|
params: ActivateMemberChannelParams,
|
|
119
125
|
): ContactWriteResult | null {
|
|
@@ -124,12 +130,7 @@ function mirrorLocalActivation(
|
|
|
124
130
|
externalChatId: params.externalChatId,
|
|
125
131
|
displayName: params.displayName,
|
|
126
132
|
username: params.username,
|
|
127
|
-
role: "contact",
|
|
128
|
-
status: "active",
|
|
129
|
-
policy: params.policy ?? "allow",
|
|
130
133
|
inviteId: params.inviteId,
|
|
131
|
-
verifiedAt: params.verifiedAt,
|
|
132
|
-
verifiedVia: params.verifiedVia ?? "invite",
|
|
133
134
|
contactId: params.contactId,
|
|
134
135
|
});
|
|
135
136
|
} catch (err) {
|
|
@@ -144,12 +145,13 @@ function mirrorLocalActivation(
|
|
|
144
145
|
// ── Revoke ───────────────────────────────────────────────────────────
|
|
145
146
|
|
|
146
147
|
/**
|
|
147
|
-
* Revoke a member channel gateway-first
|
|
148
|
-
*
|
|
149
|
-
*
|
|
148
|
+
* Revoke a member channel gateway-first. The gateway owns the ACL outcome; the
|
|
149
|
+
* memberId may be a plain channel ID or the composite contactId:channelId form
|
|
150
|
+
* revokeMember accepts.
|
|
150
151
|
*
|
|
151
|
-
* Returns the
|
|
152
|
-
*
|
|
152
|
+
* Returns the locally-resolved native contact/channel for the revoked id, or
|
|
153
|
+
* null when no local row exists. The local read is best-effort and never gates
|
|
154
|
+
* the gateway-owned downgrade.
|
|
153
155
|
*/
|
|
154
156
|
export async function revokeMemberChannel(
|
|
155
157
|
memberId: string,
|
|
@@ -158,8 +160,8 @@ export async function revokeMemberChannel(
|
|
|
158
160
|
const channelId = memberId.includes(":") ? memberId.split(":")[1] : memberId;
|
|
159
161
|
|
|
160
162
|
// Always relay; the gateway owns the ACL outcome and mark_channel_revoked is
|
|
161
|
-
// idempotent (already-revoked → didWrite:false). Skipping on the local
|
|
162
|
-
// status would suppress a needed revoke when the
|
|
163
|
+
// idempotent (already-revoked → didWrite:false). Skipping on the local row
|
|
164
|
+
// status would suppress a needed revoke when the local read lags the gateway.
|
|
163
165
|
const result = await ipcCallPersistent("mark_channel_revoked", {
|
|
164
166
|
contactChannelId: channelId,
|
|
165
167
|
reason,
|
|
@@ -169,20 +171,12 @@ export async function revokeMemberChannel(
|
|
|
169
171
|
throw new Error("mark_channel_revoked relay returned ok: false");
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
return mirrorLocalRevoke(memberId, reason);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Best-effort local mirror of the revoke. Swallows failures. */
|
|
176
|
-
function mirrorLocalRevoke(
|
|
177
|
-
memberId: string,
|
|
178
|
-
reason?: string,
|
|
179
|
-
): ContactWriteResult | null {
|
|
180
174
|
try {
|
|
181
|
-
return revokeMember(memberId
|
|
175
|
+
return revokeMember(memberId);
|
|
182
176
|
} catch (err) {
|
|
183
177
|
log.error(
|
|
184
178
|
{ err, memberId },
|
|
185
|
-
"Local revoke
|
|
179
|
+
"Local revoke read failed after gateway revoke; gateway downgrade stands",
|
|
186
180
|
);
|
|
187
181
|
return null;
|
|
188
182
|
}
|
package/src/contacts/types.ts
CHANGED
|
@@ -30,20 +30,12 @@ export interface Contact {
|
|
|
30
30
|
displayName: string;
|
|
31
31
|
/** Free-text notes about this contact (e.g. relationship, communication preferences). */
|
|
32
32
|
notes: string | null;
|
|
33
|
+
role: ContactRole;
|
|
33
34
|
lastInteraction: number | null;
|
|
34
35
|
interactionCount: number;
|
|
35
36
|
createdAt: number;
|
|
36
37
|
updatedAt: number;
|
|
37
|
-
role: ContactRole;
|
|
38
38
|
contactType: ContactType;
|
|
39
|
-
/**
|
|
40
|
-
* Internal auth identity (e.g. "vellum-principal-<uuid>"). Only meaningful
|
|
41
|
-
* for guardian contacts — it ties the contact record to the auth layer so
|
|
42
|
-
* the system can verify "this API caller IS this guardian" via JWT
|
|
43
|
-
* actorPrincipalId. Always null for non-guardian contacts, which are
|
|
44
|
-
* identified by channel address instead.
|
|
45
|
-
*/
|
|
46
|
-
principalId: string | null;
|
|
47
39
|
/** Workspace-relative path to a per-user persona file for this contact. */
|
|
48
40
|
userFile: string | null;
|
|
49
41
|
}
|
|
@@ -63,13 +55,9 @@ export interface ContactChannel {
|
|
|
63
55
|
address: string;
|
|
64
56
|
isPrimary: boolean;
|
|
65
57
|
externalChatId: string | null;
|
|
66
|
-
status: ChannelStatus;
|
|
67
|
-
policy: ChannelPolicy;
|
|
68
|
-
verifiedAt: number | null;
|
|
69
|
-
verifiedVia: string | null;
|
|
70
58
|
inviteId: string | null;
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
// INFO telemetry (not ACL): interaction stats written locally by the gateway's
|
|
60
|
+
// handle-inbound mirror. Model-facing turn context reads these.
|
|
73
61
|
lastSeenAt: number | null;
|
|
74
62
|
interactionCount: number;
|
|
75
63
|
lastInteraction: number | null;
|
|
@@ -47,20 +47,6 @@ mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
|
47
47
|
broadcastMessage: () => {},
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
-
// Control the advisor profile gate to verify the advisor tool is wired to it.
|
|
51
|
-
// The gate's own config semantics (default-on, active-profile fallback) are
|
|
52
|
-
// covered by advisor-gate.test.ts; here we only assert the wiring and the
|
|
53
|
-
// profile argument isToolActiveForContext passes through.
|
|
54
|
-
let advisorGateResult = true;
|
|
55
|
-
const advisorGateProfiles: (string | null)[] = [];
|
|
56
|
-
|
|
57
|
-
mock.module("../../plugins/defaults/advisor/advisor-gate.js", () => ({
|
|
58
|
-
advisorEnabledForProfile: (profile: string | null) => {
|
|
59
|
-
advisorGateProfiles.push(profile);
|
|
60
|
-
return advisorGateResult;
|
|
61
|
-
},
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
50
|
// Dynamic imports after mock.module calls so the stubs take effect
|
|
65
51
|
// before the modules under test are loaded.
|
|
66
52
|
const { HOST_TOOL_NAMES, HOST_TOOL_TO_CAPABILITY, isToolActiveForContext } =
|
|
@@ -611,36 +597,6 @@ describe("isToolActiveForContext — ask_question macOS gating", () => {
|
|
|
611
597
|
});
|
|
612
598
|
});
|
|
613
599
|
|
|
614
|
-
describe("isToolActiveForContext — advisor profile gate", () => {
|
|
615
|
-
beforeEach(() => {
|
|
616
|
-
advisorGateResult = true;
|
|
617
|
-
advisorGateProfiles.length = 0;
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
test("advisor is active when the profile enables it", () => {
|
|
621
|
-
advisorGateResult = true;
|
|
622
|
-
expect(isToolActiveForContext("advisor", makeCtx())).toBe(true);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("advisor is NOT active when the profile disables it", () => {
|
|
626
|
-
advisorGateResult = false;
|
|
627
|
-
expect(isToolActiveForContext("advisor", makeCtx())).toBe(false);
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
test("consults the gate with the per-turn override profile", () => {
|
|
631
|
-
isToolActiveForContext(
|
|
632
|
-
"advisor",
|
|
633
|
-
makeCtx({ currentTurnOverrideProfile: "cost-optimized" }),
|
|
634
|
-
);
|
|
635
|
-
expect(advisorGateProfiles).toEqual(["cost-optimized"]);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
test("consults the gate with null when no per-turn override is set", () => {
|
|
639
|
-
isToolActiveForContext("advisor", makeCtx());
|
|
640
|
-
expect(advisorGateProfiles).toEqual([null]);
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
|
|
644
600
|
describe("HOST_TOOL_NAMES derivation", () => {
|
|
645
601
|
test("HOST_TOOL_NAMES is derived from HOST_TOOL_TO_CAPABILITY", () => {
|
|
646
602
|
// Sanity check: every tool in the names set has a capability mapping.
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
getMessageById,
|
|
30
30
|
messageMetadataSchema,
|
|
31
31
|
provenanceFromTrustContext,
|
|
32
|
+
recordConversationPersistedSeq,
|
|
32
33
|
reserveMessage,
|
|
33
34
|
setConversationHistoryStrippedAt,
|
|
34
35
|
setLastNotifiedInferenceProfile,
|
|
@@ -57,10 +58,7 @@ import type {
|
|
|
57
58
|
ImageContent,
|
|
58
59
|
Message,
|
|
59
60
|
} from "../providers/types.js";
|
|
60
|
-
import {
|
|
61
|
-
getCurrentSeq,
|
|
62
|
-
recordPersistedSeq,
|
|
63
|
-
} from "../runtime/assistant-stream-state.js";
|
|
61
|
+
import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
|
|
64
62
|
import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
|
|
65
63
|
import { redactSecrets } from "../security/secret-scanner.js";
|
|
66
64
|
import { extractDomain } from "../tools/network/domain-normalize.js";
|
|
@@ -104,6 +102,12 @@ import type {
|
|
|
104
102
|
|
|
105
103
|
const log = getLogger("agent-loop-handlers");
|
|
106
104
|
|
|
105
|
+
function shouldPersistProviderErrorAsAssistantMessage(classified: {
|
|
106
|
+
code: string;
|
|
107
|
+
}): boolean {
|
|
108
|
+
return classified.code !== "MANAGED_KEY_INVALID";
|
|
109
|
+
}
|
|
110
|
+
|
|
107
111
|
/**
|
|
108
112
|
* Persist the history-stripped marker after the loop strips runtime injections
|
|
109
113
|
* for compaction / overflow recovery. The marker is a durability hint, not
|
|
@@ -163,6 +167,7 @@ export interface EventHandlerState {
|
|
|
163
167
|
readonly exchangeRawResponses: unknown[];
|
|
164
168
|
model: string;
|
|
165
169
|
providerErrorUserMessage: string | null;
|
|
170
|
+
persistProviderErrorAsAssistantMessage: boolean;
|
|
166
171
|
lastAssistantMessageId: string | undefined;
|
|
167
172
|
/**
|
|
168
173
|
* True when `handleLlmCallStarted` has reserved an empty assistant row
|
|
@@ -341,6 +346,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
341
346
|
exchangeRawResponses: [],
|
|
342
347
|
model: "",
|
|
343
348
|
providerErrorUserMessage: null,
|
|
349
|
+
persistProviderErrorAsAssistantMessage: false,
|
|
344
350
|
lastAssistantMessageId: undefined,
|
|
345
351
|
assistantRowAwaitingFinalization: false,
|
|
346
352
|
pendingToolResults: new Map(),
|
|
@@ -530,7 +536,7 @@ async function flushAccumulatedContent(
|
|
|
530
536
|
// Record only after the write commits, so the snapshot seq never
|
|
531
537
|
// claims content that is not yet durable.
|
|
532
538
|
if (flushedSeq != null) {
|
|
533
|
-
|
|
539
|
+
recordConversationPersistedSeq(deps.ctx.conversationId, flushedSeq);
|
|
534
540
|
}
|
|
535
541
|
} catch (err) {
|
|
536
542
|
deps.rlog.warn(
|
|
@@ -918,7 +924,7 @@ export function handleToolUse(
|
|
|
918
924
|
// persisted seq to it. Without this the snapshot would advertise a seq below
|
|
919
925
|
// an event it already incorporates, and a client applying `seq > snapshot.seq`
|
|
920
926
|
// would replay this tool start.
|
|
921
|
-
|
|
927
|
+
recordConversationPersistedSeq(deps.ctx.conversationId, getCurrentSeq());
|
|
922
928
|
}
|
|
923
929
|
|
|
924
930
|
export function handleToolUsePreviewStart(
|
|
@@ -1136,7 +1142,7 @@ async function persistPendingToolResultRow(
|
|
|
1136
1142
|
rowId,
|
|
1137
1143
|
JSON.stringify(buildToolResultBlocks(state.pendingToolResults)),
|
|
1138
1144
|
);
|
|
1139
|
-
|
|
1145
|
+
recordConversationPersistedSeq(deps.ctx.conversationId, seq);
|
|
1140
1146
|
const conv = getConversation(deps.ctx.conversationId);
|
|
1141
1147
|
if (conv != null) {
|
|
1142
1148
|
syncMessageToDisk(deps.ctx.conversationId, rowId, conv.createdAt);
|
|
@@ -1618,6 +1624,8 @@ function handleError(
|
|
|
1618
1624
|
buildConversationErrorMessage(deps.ctx.conversationId, classified),
|
|
1619
1625
|
);
|
|
1620
1626
|
state.providerErrorUserMessage = classified.userMessage;
|
|
1627
|
+
state.persistProviderErrorAsAssistantMessage =
|
|
1628
|
+
shouldPersistProviderErrorAsAssistantMessage(classified);
|
|
1621
1629
|
}
|
|
1622
1630
|
|
|
1623
1631
|
export function handleMaxTokensReached(
|
|
@@ -1816,10 +1824,14 @@ export async function handleMessageComplete(
|
|
|
1816
1824
|
// delta's seq -- the highest stamped content event this row reflects -- so
|
|
1817
1825
|
// recording it is honest. A drained tool result was stamped earlier in the
|
|
1818
1826
|
// turn, so this seq already covers it; a call that streams no content (a
|
|
1819
|
-
// pure tool call) advances instead via `tool_use_start`.
|
|
1820
|
-
// clamps monotonically, so a lower value
|
|
1827
|
+
// pure tool call) advances instead via `tool_use_start`.
|
|
1828
|
+
// `recordConversationPersistedSeq` clamps monotonically, so a lower value
|
|
1829
|
+
// here never regresses the seq.
|
|
1821
1830
|
if (state.lastPersistedContentSeq != null) {
|
|
1822
|
-
|
|
1831
|
+
recordConversationPersistedSeq(
|
|
1832
|
+
deps.ctx.conversationId,
|
|
1833
|
+
state.lastPersistedContentSeq,
|
|
1834
|
+
);
|
|
1823
1835
|
}
|
|
1824
1836
|
// Reset the partial-persist mirror so subsequent calls in this turn
|
|
1825
1837
|
// start with an empty running view.
|
|
@@ -2127,6 +2139,13 @@ function handleProviderError(
|
|
|
2127
2139
|
deps: EventHandlerDeps,
|
|
2128
2140
|
event: Extract<AgentEvent, { type: "provider_error" }>,
|
|
2129
2141
|
): void {
|
|
2142
|
+
const classified = classifyConversationError(event.error, {
|
|
2143
|
+
phase: "agent_loop",
|
|
2144
|
+
});
|
|
2145
|
+
if (!shouldPersistProviderErrorAsAssistantMessage(classified)) {
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2130
2149
|
try {
|
|
2131
2150
|
recordRequestLog(
|
|
2132
2151
|
deps.ctx.conversationId,
|