@vellumai/assistant 0.10.3-staging.2 → 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
|
@@ -40,14 +40,13 @@ mock.module("../../ipc/gateway-client.js", () => ({
|
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
// Local
|
|
43
|
+
// Local resolver primitive — resolves the native contact/channel by id; the
|
|
44
|
+
// gateway owns the ACL downgrade, so it takes no reason and mutates nothing.
|
|
44
45
|
const revokeMemberResult: ContactWriteResult = {
|
|
45
46
|
contact: { id: "c1" } as ContactWriteResult["contact"],
|
|
46
|
-
channel: { id: "ch1"
|
|
47
|
+
channel: { id: "ch1" } as ContactWriteResult["channel"],
|
|
47
48
|
};
|
|
48
|
-
const revokeMemberMock = mock((_memberId: string
|
|
49
|
-
revokeMemberResult,
|
|
50
|
-
);
|
|
49
|
+
const revokeMemberMock = mock((_memberId: string) => revokeMemberResult);
|
|
51
50
|
const actualContactsWrite = await import("../contacts-write.js");
|
|
52
51
|
mock.module("../contacts-write.js", () => ({
|
|
53
52
|
...actualContactsWrite,
|
|
@@ -75,7 +74,7 @@ describe("revokeMemberChannel gateway-first relay", () => {
|
|
|
75
74
|
},
|
|
76
75
|
]);
|
|
77
76
|
expect(revokeMemberMock).toHaveBeenCalledTimes(1);
|
|
78
|
-
expect(revokeMemberMock).toHaveBeenCalledWith("ch1"
|
|
77
|
+
expect(revokeMemberMock).toHaveBeenCalledWith("ch1");
|
|
79
78
|
expect(result).toBe(revokeMemberResult);
|
|
80
79
|
});
|
|
81
80
|
|
|
@@ -83,8 +82,8 @@ describe("revokeMemberChannel gateway-first relay", () => {
|
|
|
83
82
|
await revokeMemberChannel("c1:ch1");
|
|
84
83
|
|
|
85
84
|
expect(ipcCalls[0]?.params?.contactChannelId).toBe("ch1");
|
|
86
|
-
// The local
|
|
87
|
-
expect(revokeMemberMock).toHaveBeenCalledWith("c1:ch1"
|
|
85
|
+
// The local resolver still receives the original composite id it accepts.
|
|
86
|
+
expect(revokeMemberMock).toHaveBeenCalledWith("c1:ch1");
|
|
88
87
|
});
|
|
89
88
|
|
|
90
89
|
test("always relays — never skips based on local mirror status", async () => {
|
|
@@ -48,7 +48,7 @@ mock.module("../../ipc/gateway-client.js", () => ({
|
|
|
48
48
|
// Local-mirror primitive.
|
|
49
49
|
const localResult: ContactWriteResult = {
|
|
50
50
|
contact: { id: "c1" } as ContactWriteResult["contact"],
|
|
51
|
-
channel: { id: "ch1"
|
|
51
|
+
channel: { id: "ch1" } as ContactWriteResult["channel"],
|
|
52
52
|
};
|
|
53
53
|
let mirrorCallOrder = -1;
|
|
54
54
|
const upsertContactChannelMock = mock(
|
|
@@ -108,6 +108,32 @@ describe("activateMemberChannel gateway-first relay", () => {
|
|
|
108
108
|
// The local mirror ran AFTER the gateway relay.
|
|
109
109
|
expect(mirrorCallOrder).toBe(1);
|
|
110
110
|
expect(upsertContactChannelMock).toHaveBeenCalledTimes(1);
|
|
111
|
+
|
|
112
|
+
// The local mirror persists identity/INFO only — no ACL columns. The
|
|
113
|
+
// gateway owns status/policy/verification.
|
|
114
|
+
const mirrorArgs = upsertContactChannelMock.mock.calls[0]![0] as Record<
|
|
115
|
+
string,
|
|
116
|
+
unknown
|
|
117
|
+
>;
|
|
118
|
+
expect(mirrorArgs).toEqual({
|
|
119
|
+
sourceChannel: "telegram",
|
|
120
|
+
externalUserId: "user-1",
|
|
121
|
+
externalChatId: "chat-1",
|
|
122
|
+
displayName: "Mom",
|
|
123
|
+
username: undefined,
|
|
124
|
+
inviteId: "inv-1",
|
|
125
|
+
contactId: "target-mom",
|
|
126
|
+
});
|
|
127
|
+
for (const aclKey of [
|
|
128
|
+
"status",
|
|
129
|
+
"policy",
|
|
130
|
+
"role",
|
|
131
|
+
"verifiedAt",
|
|
132
|
+
"verifiedVia",
|
|
133
|
+
]) {
|
|
134
|
+
expect(aclKey in mirrorArgs).toBe(false);
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
expect(result).toEqual({
|
|
112
138
|
status: "activated",
|
|
113
139
|
memberId: "ch1",
|
|
@@ -115,7 +141,7 @@ describe("activateMemberChannel gateway-first relay", () => {
|
|
|
115
141
|
});
|
|
116
142
|
});
|
|
117
143
|
|
|
118
|
-
test("fails
|
|
144
|
+
test("fails closed and skips the local mirror when the gateway relay throws", async () => {
|
|
119
145
|
ipcThrows = true;
|
|
120
146
|
|
|
121
147
|
const result = await activateMemberChannel({
|
|
@@ -126,12 +152,10 @@ describe("activateMemberChannel gateway-first relay", () => {
|
|
|
126
152
|
});
|
|
127
153
|
|
|
128
154
|
expect(ipcCalls).toHaveLength(1);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
member: localResult,
|
|
134
|
-
});
|
|
155
|
+
// Identity-only mirror would land at the schema-default unverified status, so
|
|
156
|
+
// a failed gateway write must not report success off it.
|
|
157
|
+
expect(upsertContactChannelMock).not.toHaveBeenCalled();
|
|
158
|
+
expect(result).toEqual({ status: "refused" });
|
|
135
159
|
});
|
|
136
160
|
|
|
137
161
|
test("returns the gateway channel id when the gateway verifies but the local mirror throws", async () => {
|
|
@@ -155,7 +179,7 @@ describe("activateMemberChannel gateway-first relay", () => {
|
|
|
155
179
|
});
|
|
156
180
|
});
|
|
157
181
|
|
|
158
|
-
test("refuses when the gateway throws
|
|
182
|
+
test("refuses when the gateway throws even if the local mirror would have thrown", async () => {
|
|
159
183
|
ipcThrows = true;
|
|
160
184
|
upsertContactChannelMock.mockImplementation(() => {
|
|
161
185
|
throw new Error("local mirror exploded");
|
|
@@ -167,8 +191,8 @@ describe("activateMemberChannel gateway-first relay", () => {
|
|
|
167
191
|
externalChatId: "chat-1",
|
|
168
192
|
});
|
|
169
193
|
|
|
170
|
-
//
|
|
171
|
-
|
|
194
|
+
// Fail-closed: a thrown gateway write refuses before the mirror is touched.
|
|
195
|
+
expect(upsertContactChannelMock).not.toHaveBeenCalled();
|
|
172
196
|
expect(result).toEqual({ status: "refused" });
|
|
173
197
|
});
|
|
174
198
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, asc, desc, eq,
|
|
1
|
+
import { and, asc, desc, eq, like, sql } from "drizzle-orm";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import type { ChannelId } from "../channels/types.js";
|
|
@@ -96,13 +96,12 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
|
|
|
96
96
|
id: row.id,
|
|
97
97
|
displayName: row.displayName,
|
|
98
98
|
notes: row.notes,
|
|
99
|
+
role: row.role,
|
|
99
100
|
lastInteraction: null,
|
|
100
101
|
interactionCount: 0,
|
|
101
102
|
createdAt: row.createdAt,
|
|
102
103
|
updatedAt: row.updatedAt,
|
|
103
|
-
|
|
104
|
-
contactType: (row.contactType as Contact["contactType"]) ?? "human",
|
|
105
|
-
principalId: row.principalId,
|
|
104
|
+
contactType: row.contactType,
|
|
106
105
|
userFile: row.userFile ?? null,
|
|
107
106
|
};
|
|
108
107
|
}
|
|
@@ -117,13 +116,7 @@ function parseChannel(
|
|
|
117
116
|
address: row.address,
|
|
118
117
|
isPrimary: row.isPrimary,
|
|
119
118
|
externalChatId: row.externalChatId,
|
|
120
|
-
status: row.status as ContactChannel["status"],
|
|
121
|
-
policy: row.policy as ContactChannel["policy"],
|
|
122
|
-
verifiedAt: row.verifiedAt,
|
|
123
|
-
verifiedVia: row.verifiedVia,
|
|
124
119
|
inviteId: row.inviteId,
|
|
125
|
-
revokedReason: row.revokedReason,
|
|
126
|
-
blockedReason: row.blockedReason,
|
|
127
120
|
lastSeenAt: row.lastSeenAt,
|
|
128
121
|
interactionCount: row.interactionCount,
|
|
129
122
|
lastInteraction: row.lastInteraction,
|
|
@@ -145,6 +138,8 @@ function getChannelsForContact(contactId: string): ContactChannel[] {
|
|
|
145
138
|
|
|
146
139
|
function withChannels(contact: Contact): ContactWithChannels {
|
|
147
140
|
const channels = getChannelsForContact(contact.id);
|
|
141
|
+
// INFO telemetry aggregated from channel rows (not ACL): sum interaction
|
|
142
|
+
// counts, take the most recent interaction across channels.
|
|
148
143
|
const interactionCount = channels.reduce(
|
|
149
144
|
(sum, ch) => sum + ch.interactionCount,
|
|
150
145
|
0,
|
|
@@ -225,7 +220,6 @@ export function upsertContact(params: {
|
|
|
225
220
|
notes?: string | null;
|
|
226
221
|
role?: ContactRole;
|
|
227
222
|
contactType?: ContactType;
|
|
228
|
-
principalId?: string | null;
|
|
229
223
|
userFile?: string | null;
|
|
230
224
|
channels?: SyncChannelData[];
|
|
231
225
|
/** When true, conflicting channels on other contacts are reassigned to this
|
|
@@ -260,11 +254,8 @@ export function upsertContact(params: {
|
|
|
260
254
|
updatedAt: now,
|
|
261
255
|
};
|
|
262
256
|
if (params.notes !== undefined) updateSet.notes = params.notes;
|
|
263
|
-
if (params.role !== undefined) updateSet.role = params.role;
|
|
264
257
|
if (params.contactType !== undefined)
|
|
265
258
|
updateSet.contactType = params.contactType;
|
|
266
|
-
if (params.principalId !== undefined)
|
|
267
|
-
updateSet.principalId = params.principalId;
|
|
268
259
|
if (params.userFile !== undefined) updateSet.userFile = params.userFile;
|
|
269
260
|
|
|
270
261
|
db.update(contacts)
|
|
@@ -298,11 +289,8 @@ export function upsertContact(params: {
|
|
|
298
289
|
updatedAt: now,
|
|
299
290
|
};
|
|
300
291
|
if (params.notes !== undefined) updateSet.notes = params.notes;
|
|
301
|
-
if (params.role !== undefined) updateSet.role = params.role;
|
|
302
292
|
if (params.contactType !== undefined)
|
|
303
293
|
updateSet.contactType = params.contactType;
|
|
304
|
-
if (params.principalId !== undefined)
|
|
305
|
-
updateSet.principalId = params.principalId;
|
|
306
294
|
if (params.userFile !== undefined) updateSet.userFile = params.userFile;
|
|
307
295
|
|
|
308
296
|
db.update(contacts)
|
|
@@ -319,35 +307,16 @@ export function upsertContact(params: {
|
|
|
319
307
|
|
|
320
308
|
// Create new contact
|
|
321
309
|
contactId = contactId ?? uuid();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
resolvedUserFile = params.userFile;
|
|
327
|
-
} else if (params.principalId) {
|
|
328
|
-
const sibling = db
|
|
329
|
-
.select({ userFile: contacts.userFile })
|
|
330
|
-
.from(contacts)
|
|
331
|
-
.where(
|
|
332
|
-
and(
|
|
333
|
-
eq(contacts.principalId, params.principalId),
|
|
334
|
-
isNotNull(contacts.userFile),
|
|
335
|
-
),
|
|
336
|
-
)
|
|
337
|
-
.get();
|
|
338
|
-
resolvedUserFile =
|
|
339
|
-
sibling?.userFile ?? generateUserFileSlug(params.displayName);
|
|
340
|
-
} else {
|
|
341
|
-
resolvedUserFile = generateUserFileSlug(params.displayName);
|
|
342
|
-
}
|
|
310
|
+
const resolvedUserFile =
|
|
311
|
+
params.userFile !== undefined
|
|
312
|
+
? params.userFile
|
|
313
|
+
: generateUserFileSlug(params.displayName);
|
|
343
314
|
db.insert(contacts)
|
|
344
315
|
.values({
|
|
345
316
|
id: contactId,
|
|
346
317
|
displayName: params.displayName,
|
|
347
318
|
notes: params.notes ?? null,
|
|
348
|
-
role: params.role ?? "contact",
|
|
349
319
|
contactType: params.contactType ?? "human",
|
|
350
|
-
principalId: params.principalId ?? null,
|
|
351
320
|
userFile: resolvedUserFile,
|
|
352
321
|
createdAt: now,
|
|
353
322
|
updatedAt: now,
|
|
@@ -396,27 +365,12 @@ function syncChannels(
|
|
|
396
365
|
.get();
|
|
397
366
|
|
|
398
367
|
if (existing) {
|
|
399
|
-
// Preserve guardian blocks: if the channel is blocked, do not overwrite
|
|
400
|
-
// its status/policy — mirrors the guard in the cross-contact reassignment
|
|
401
|
-
// path so a blocked channel cannot be unblocked via a same-contact sync.
|
|
402
|
-
const isBlocked = existing.status === "blocked";
|
|
403
|
-
|
|
404
368
|
const updateSet: Record<string, unknown> = {};
|
|
405
369
|
// Self-heal legacy lowercased addresses to canonical form.
|
|
406
370
|
if (existing.address !== ch.address) updateSet.address = ch.address;
|
|
407
371
|
if (ch.isPrimary !== undefined) updateSet.isPrimary = ch.isPrimary;
|
|
408
372
|
if (ch.externalChatId !== undefined)
|
|
409
373
|
updateSet.externalChatId = ch.externalChatId;
|
|
410
|
-
if (!isBlocked) {
|
|
411
|
-
if (ch.status !== undefined) updateSet.status = ch.status;
|
|
412
|
-
if (ch.policy !== undefined) updateSet.policy = ch.policy;
|
|
413
|
-
if (ch.revokedReason !== undefined)
|
|
414
|
-
updateSet.revokedReason = ch.revokedReason;
|
|
415
|
-
if (ch.blockedReason !== undefined)
|
|
416
|
-
updateSet.blockedReason = ch.blockedReason;
|
|
417
|
-
}
|
|
418
|
-
if (ch.verifiedAt !== undefined) updateSet.verifiedAt = ch.verifiedAt;
|
|
419
|
-
if (ch.verifiedVia !== undefined) updateSet.verifiedVia = ch.verifiedVia;
|
|
420
374
|
if (ch.inviteId !== undefined) updateSet.inviteId = ch.inviteId;
|
|
421
375
|
|
|
422
376
|
if (Object.keys(updateSet).length > 0) {
|
|
@@ -434,11 +388,6 @@ function syncChannels(
|
|
|
434
388
|
|
|
435
389
|
if (conflicting) {
|
|
436
390
|
if (reassignConflicting) {
|
|
437
|
-
// Preserve guardian blocks: if the existing channel is blocked, do not
|
|
438
|
-
// overwrite its status/policy — a valid invite must not bypass an
|
|
439
|
-
// explicit guardian block on a different contact.
|
|
440
|
-
const isBlocked = conflicting.status === "blocked";
|
|
441
|
-
|
|
442
391
|
// Reassign the channel to the target contact. Used by invite redemption
|
|
443
392
|
// to bind a redeemer's existing channel identity to the invite's target.
|
|
444
393
|
const reassignSet: Record<string, unknown> = {
|
|
@@ -447,17 +396,6 @@ function syncChannels(
|
|
|
447
396
|
};
|
|
448
397
|
if (ch.externalChatId !== undefined)
|
|
449
398
|
reassignSet.externalChatId = ch.externalChatId;
|
|
450
|
-
if (!isBlocked) {
|
|
451
|
-
if (ch.status !== undefined) reassignSet.status = ch.status;
|
|
452
|
-
if (ch.policy !== undefined) reassignSet.policy = ch.policy;
|
|
453
|
-
if (ch.revokedReason !== undefined)
|
|
454
|
-
reassignSet.revokedReason = ch.revokedReason;
|
|
455
|
-
if (ch.blockedReason !== undefined)
|
|
456
|
-
reassignSet.blockedReason = ch.blockedReason;
|
|
457
|
-
}
|
|
458
|
-
if (ch.verifiedAt !== undefined) reassignSet.verifiedAt = ch.verifiedAt;
|
|
459
|
-
if (ch.verifiedVia !== undefined)
|
|
460
|
-
reassignSet.verifiedVia = ch.verifiedVia;
|
|
461
399
|
if (ch.inviteId !== undefined) reassignSet.inviteId = ch.inviteId;
|
|
462
400
|
|
|
463
401
|
db.update(contactChannels)
|
|
@@ -478,10 +416,6 @@ function syncChannels(
|
|
|
478
416
|
address: ch.address,
|
|
479
417
|
isPrimary: ch.isPrimary ?? false,
|
|
480
418
|
externalChatId: ch.externalChatId ?? null,
|
|
481
|
-
status: ch.status ?? "unverified",
|
|
482
|
-
policy: ch.policy ?? "allow",
|
|
483
|
-
verifiedAt: ch.verifiedAt ?? null,
|
|
484
|
-
verifiedVia: ch.verifiedVia ?? null,
|
|
485
419
|
inviteId: ch.inviteId ?? null,
|
|
486
420
|
createdAt: now,
|
|
487
421
|
updatedAt: now,
|
|
@@ -494,7 +428,6 @@ export function searchContacts(params: {
|
|
|
494
428
|
query?: string;
|
|
495
429
|
channelAddress?: string;
|
|
496
430
|
channelType?: string;
|
|
497
|
-
role?: ContactRole;
|
|
498
431
|
contactType?: ContactType;
|
|
499
432
|
limit?: number;
|
|
500
433
|
}): ContactWithChannels[] {
|
|
@@ -534,7 +467,6 @@ export function searchContacts(params: {
|
|
|
534
467
|
const contact = getContactInternal(id);
|
|
535
468
|
if (
|
|
536
469
|
contact &&
|
|
537
|
-
(!params.role || contact.role === params.role) &&
|
|
538
470
|
(!params.contactType || contact.contactType === params.contactType) &&
|
|
539
471
|
(!sanitizedQuery ||
|
|
540
472
|
(contact.displayName &&
|
|
@@ -564,7 +496,6 @@ export function searchContacts(params: {
|
|
|
564
496
|
const contact = getContactInternal(id);
|
|
565
497
|
if (
|
|
566
498
|
contact &&
|
|
567
|
-
(!params.role || contact.role === params.role) &&
|
|
568
499
|
(!params.contactType || contact.contactType === params.contactType)
|
|
569
500
|
) {
|
|
570
501
|
results.push(contact);
|
|
@@ -577,14 +508,11 @@ export function searchContacts(params: {
|
|
|
577
508
|
const conditions = [];
|
|
578
509
|
if (params.query) {
|
|
579
510
|
const sanitized = escapeLike(params.query);
|
|
580
|
-
if (!sanitized && !params.
|
|
511
|
+
if (!sanitized && !params.contactType) return [];
|
|
581
512
|
if (sanitized) {
|
|
582
513
|
conditions.push(like(contacts.displayName, `%${sanitized}%`));
|
|
583
514
|
}
|
|
584
515
|
}
|
|
585
|
-
if (params.role) {
|
|
586
|
-
conditions.push(eq(contacts.role, params.role));
|
|
587
|
-
}
|
|
588
516
|
if (params.contactType) {
|
|
589
517
|
conditions.push(eq(contacts.contactType, params.contactType));
|
|
590
518
|
}
|
|
@@ -633,20 +561,16 @@ export function searchContacts(params: {
|
|
|
633
561
|
|
|
634
562
|
export function listContacts(
|
|
635
563
|
limit = 50,
|
|
636
|
-
role?: ContactRole,
|
|
637
564
|
contactType?: ContactType,
|
|
638
565
|
opts?: { uncapped?: boolean },
|
|
639
566
|
): ContactWithChannels[] {
|
|
640
567
|
const db = getDb();
|
|
641
568
|
const effectiveLimit = opts?.uncapped ? limit : Math.min(limit, 200);
|
|
642
|
-
const conditions = [];
|
|
643
|
-
if (role) conditions.push(eq(contacts.role, role));
|
|
644
|
-
if (contactType) conditions.push(eq(contacts.contactType, contactType));
|
|
645
569
|
const rows = db
|
|
646
570
|
.select()
|
|
647
571
|
.from(contacts)
|
|
648
|
-
.where(
|
|
649
|
-
.orderBy(
|
|
572
|
+
.where(contactType ? eq(contacts.contactType, contactType) : undefined)
|
|
573
|
+
.orderBy(desc(contacts.updatedAt))
|
|
650
574
|
.limit(effectiveLimit)
|
|
651
575
|
.all();
|
|
652
576
|
return rows.map((r) => withChannels(parseContact(r)));
|
|
@@ -758,7 +682,8 @@ export function findContactByAddress(
|
|
|
758
682
|
/**
|
|
759
683
|
* Find a contact by channel external chat ID. Fallback for callers that only
|
|
760
684
|
* have a chat ID (no user-level address) — matches by (type, externalChatId).
|
|
761
|
-
* No unique constraint exists on externalChatId, so ORDER BY is needed
|
|
685
|
+
* No unique constraint exists on externalChatId, so ORDER BY is needed for a
|
|
686
|
+
* deterministic pick; channel ranking (status) is owned by the gateway now.
|
|
762
687
|
*/
|
|
763
688
|
function findContactByChannelExternalChatId(
|
|
764
689
|
channelType: string,
|
|
@@ -774,14 +699,7 @@ function findContactByChannelExternalChatId(
|
|
|
774
699
|
eq(contactChannels.externalChatId, externalChatId),
|
|
775
700
|
),
|
|
776
701
|
)
|
|
777
|
-
.orderBy(
|
|
778
|
-
sql`CASE ${contactChannels.status}
|
|
779
|
-
WHEN 'active' THEN 0
|
|
780
|
-
WHEN 'unverified' THEN 1
|
|
781
|
-
ELSE 2
|
|
782
|
-
END`,
|
|
783
|
-
desc(contactChannels.updatedAt),
|
|
784
|
-
)
|
|
702
|
+
.orderBy(desc(contactChannels.updatedAt), desc(contactChannels.createdAt))
|
|
785
703
|
.get();
|
|
786
704
|
if (!channel) return null;
|
|
787
705
|
return getContactInternal(channel.contactId);
|
|
@@ -830,131 +748,10 @@ export function findContactChannel(params: {
|
|
|
830
748
|
}
|
|
831
749
|
|
|
832
750
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
|
|
837
|
-
export function findGuardianForChannel(
|
|
838
|
-
channelType: string,
|
|
839
|
-
): { contact: Contact; channel: ContactChannel } | null {
|
|
840
|
-
const db = getDb();
|
|
841
|
-
const conditions = [
|
|
842
|
-
eq(contacts.role, "guardian"),
|
|
843
|
-
eq(contactChannels.type, channelType),
|
|
844
|
-
eq(contactChannels.status, "active"),
|
|
845
|
-
];
|
|
846
|
-
const rows = db
|
|
847
|
-
.select({
|
|
848
|
-
contact: contacts,
|
|
849
|
-
channel: contactChannels,
|
|
850
|
-
})
|
|
851
|
-
.from(contacts)
|
|
852
|
-
.innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
|
|
853
|
-
.where(and(...conditions))
|
|
854
|
-
.orderBy(desc(contactChannels.verifiedAt))
|
|
855
|
-
.limit(1)
|
|
856
|
-
.all();
|
|
857
|
-
|
|
858
|
-
if (rows.length === 0) return null;
|
|
859
|
-
const row = rows[0];
|
|
860
|
-
return {
|
|
861
|
-
contact: parseContact(row.contact),
|
|
862
|
-
channel: parseChannel(row.channel),
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
/**
|
|
867
|
-
* List all active channels for guardian contacts.
|
|
868
|
-
* This is the contacts-based equivalent of listActiveBindingsByAssistant(assistantId).
|
|
869
|
-
* Joins contacts+channels with status='active' in a single query so we never
|
|
870
|
-
* pick a guardian that has no active channels.
|
|
871
|
-
* Returns channels ordered by most-recently-verified first.
|
|
872
|
-
*/
|
|
873
|
-
export function listGuardianChannels(): {
|
|
874
|
-
contact: Contact;
|
|
875
|
-
channels: ContactChannel[];
|
|
876
|
-
} | null {
|
|
877
|
-
const db = getDb();
|
|
878
|
-
const rows = db
|
|
879
|
-
.select({
|
|
880
|
-
contact: contacts,
|
|
881
|
-
channel: contactChannels,
|
|
882
|
-
})
|
|
883
|
-
.from(contacts)
|
|
884
|
-
.innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
|
|
885
|
-
.where(
|
|
886
|
-
and(eq(contacts.role, "guardian"), eq(contactChannels.status, "active")),
|
|
887
|
-
)
|
|
888
|
-
.orderBy(desc(contactChannels.verifiedAt))
|
|
889
|
-
.all();
|
|
890
|
-
|
|
891
|
-
if (rows.length === 0) return null;
|
|
892
|
-
|
|
893
|
-
// Use the first row's contact (the guardian with the most-recently-verified
|
|
894
|
-
// active channel) and collect all active channels for that contact.
|
|
895
|
-
const guardian = parseContact(rows[0].contact);
|
|
896
|
-
const channels = rows
|
|
897
|
-
.filter((r) => r.contact.id === guardian.id)
|
|
898
|
-
.map((r) => parseChannel(r.channel));
|
|
899
|
-
|
|
900
|
-
return { contact: guardian, channels };
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Update a channel's access-control fields (status, policy, reasons).
|
|
905
|
-
* Returns the updated channel, or null if the channel does not exist.
|
|
906
|
-
*/
|
|
907
|
-
export function updateChannelStatus(
|
|
908
|
-
channelId: string,
|
|
909
|
-
params: {
|
|
910
|
-
status?: ChannelStatus;
|
|
911
|
-
policy?: ChannelPolicy;
|
|
912
|
-
revokedReason?: string | null;
|
|
913
|
-
blockedReason?: string | null;
|
|
914
|
-
},
|
|
915
|
-
): ContactChannel | null {
|
|
916
|
-
const db = getDb();
|
|
917
|
-
const existing = db
|
|
918
|
-
.select()
|
|
919
|
-
.from(contactChannels)
|
|
920
|
-
.where(eq(contactChannels.id, channelId))
|
|
921
|
-
.get();
|
|
922
|
-
|
|
923
|
-
if (!existing) return null;
|
|
924
|
-
|
|
925
|
-
const updateSet: Record<string, unknown> = {};
|
|
926
|
-
if (params.status !== undefined) updateSet.status = params.status;
|
|
927
|
-
if (params.policy !== undefined) updateSet.policy = params.policy;
|
|
928
|
-
if (params.revokedReason !== undefined)
|
|
929
|
-
updateSet.revokedReason = params.revokedReason;
|
|
930
|
-
if (params.blockedReason !== undefined)
|
|
931
|
-
updateSet.blockedReason = params.blockedReason;
|
|
932
|
-
|
|
933
|
-
if (Object.keys(updateSet).length > 0) {
|
|
934
|
-
updateSet.updatedAt = Date.now();
|
|
935
|
-
db.update(contactChannels)
|
|
936
|
-
.set(updateSet)
|
|
937
|
-
.where(eq(contactChannels.id, channelId))
|
|
938
|
-
.run();
|
|
939
|
-
|
|
940
|
-
const updated = db
|
|
941
|
-
.select()
|
|
942
|
-
.from(contactChannels)
|
|
943
|
-
.where(eq(contactChannels.id, channelId))
|
|
944
|
-
.get();
|
|
945
|
-
|
|
946
|
-
const result = updated ? parseChannel(updated) : null;
|
|
947
|
-
emitContactChange();
|
|
948
|
-
return result;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
return parseChannel(existing);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/**
|
|
955
|
-
* Update a guardian contact's principalId and its channel's identity fields.
|
|
956
|
-
* Used for healing guardian binding drift when the JWT principal no longer
|
|
957
|
-
* matches the stored guardian binding after a DB reset.
|
|
751
|
+
* Heal a guardian channel's identity address when the JWT principal no longer
|
|
752
|
+
* matches the stored guardian binding after a DB reset. The principalId ACL
|
|
753
|
+
* column is gateway-owned and no longer written here; only the channel identity
|
|
754
|
+
* address is repaired.
|
|
958
755
|
*
|
|
959
756
|
* Returns false if the update would violate the unique (type, address)
|
|
960
757
|
* constraint on contact_channels — e.g. when the incoming principal already
|
|
@@ -962,7 +759,7 @@ export function updateChannelStatus(
|
|
|
962
759
|
* In that case the heal is skipped and trust stays `unknown`.
|
|
963
760
|
*/
|
|
964
761
|
export function updateContactPrincipalAndChannel(
|
|
965
|
-
|
|
762
|
+
_contactId: string,
|
|
966
763
|
channelId: string,
|
|
967
764
|
newPrincipalId: string,
|
|
968
765
|
): boolean {
|
|
@@ -983,20 +780,13 @@ export function updateContactPrincipalAndChannel(
|
|
|
983
780
|
return false;
|
|
984
781
|
}
|
|
985
782
|
|
|
986
|
-
db.
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
.set({
|
|
994
|
-
address: newPrincipalId,
|
|
995
|
-
updatedAt: now,
|
|
996
|
-
})
|
|
997
|
-
.where(eq(contactChannels.id, channelId))
|
|
998
|
-
.run();
|
|
999
|
-
});
|
|
783
|
+
db.update(contactChannels)
|
|
784
|
+
.set({
|
|
785
|
+
address: newPrincipalId,
|
|
786
|
+
updatedAt: now,
|
|
787
|
+
})
|
|
788
|
+
.where(eq(contactChannels.id, channelId))
|
|
789
|
+
.run();
|
|
1000
790
|
|
|
1001
791
|
emitContactChange();
|
|
1002
792
|
return true;
|