@vellumai/assistant 0.4.49 → 0.4.50
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildTemporalContext,
|
|
5
|
-
|
|
5
|
+
extractUserTimeZoneFromRecall,
|
|
6
6
|
} from "../daemon/date-context.js";
|
|
7
7
|
|
|
8
8
|
// Fixed timestamps for deterministic assertions (all UTC midday to avoid DST edge cases).
|
|
@@ -152,82 +152,6 @@ describe("buildTemporalContext", () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
describe("extractUserTimeZoneFromDynamicProfile", () => {
|
|
156
|
-
test("extracts canonical timezone from explicit timezone profile line", () => {
|
|
157
|
-
const profile = [
|
|
158
|
-
"<dynamic-user-profile>",
|
|
159
|
-
"- timezone: Timezone is America/New_York.",
|
|
160
|
-
"</dynamic-user-profile>",
|
|
161
|
-
].join("\n");
|
|
162
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe(
|
|
163
|
-
"America/New_York",
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("extracts timezone token from generic profile text when explicit line is absent", () => {
|
|
168
|
-
const profile = [
|
|
169
|
-
"<dynamic-user-profile>",
|
|
170
|
-
"- location: Travels often between Europe and Asia (currently Europe/Paris).",
|
|
171
|
-
"</dynamic-user-profile>",
|
|
172
|
-
].join("\n");
|
|
173
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Europe/Paris");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("returns null when no valid timezone is present", () => {
|
|
177
|
-
const profile = [
|
|
178
|
-
"<dynamic-user-profile>",
|
|
179
|
-
"- timezone: Pacific time",
|
|
180
|
-
"</dynamic-user-profile>",
|
|
181
|
-
].join("\n");
|
|
182
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBeNull();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("extracts UTC/GMT offset tokens from explicit timezone profile line", () => {
|
|
186
|
-
const profile = [
|
|
187
|
-
"<dynamic-user-profile>",
|
|
188
|
-
"- timezone: UTC+2",
|
|
189
|
-
"</dynamic-user-profile>",
|
|
190
|
-
].join("\n");
|
|
191
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Etc/GMT-2");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("extracts GMT negative offset tokens from generic profile text", () => {
|
|
195
|
-
const profile = [
|
|
196
|
-
"<dynamic-user-profile>",
|
|
197
|
-
"- preferences: schedule notifications in GMT-5 whenever possible.",
|
|
198
|
-
"</dynamic-user-profile>",
|
|
199
|
-
].join("\n");
|
|
200
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Etc/GMT+5");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("extracts fractional UTC offset tokens from explicit timezone profile line", () => {
|
|
204
|
-
const profile = [
|
|
205
|
-
"<dynamic-user-profile>",
|
|
206
|
-
"- timezone: UTC+5:30",
|
|
207
|
-
"</dynamic-user-profile>",
|
|
208
|
-
].join("\n");
|
|
209
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("+05:30");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("extracts fractional GMT offset tokens from generic profile text", () => {
|
|
213
|
-
const profile = [
|
|
214
|
-
"<dynamic-user-profile>",
|
|
215
|
-
"- preferences: default reminders to GMT+5:45.",
|
|
216
|
-
"</dynamic-user-profile>",
|
|
217
|
-
].join("\n");
|
|
218
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("+05:45");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("prefers IANA timezone tokens over UTC/GMT offsets in the same profile line", () => {
|
|
222
|
-
const profile = [
|
|
223
|
-
"<dynamic-user-profile>",
|
|
224
|
-
"- timezone: UTC+1 (Europe/Paris)",
|
|
225
|
-
"</dynamic-user-profile>",
|
|
226
|
-
].join("\n");
|
|
227
|
-
expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Europe/Paris");
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
155
|
// ---------------------------------------------------------------------------
|
|
232
156
|
// Weekday baseline — today is Wednesday
|
|
233
157
|
// ---------------------------------------------------------------------------
|
|
@@ -615,3 +539,95 @@ describe("trip-planning: timezone-shifted weekend anchors", () => {
|
|
|
615
539
|
expect(result).toContain("Next weekend: 2026-02-28 – 2026-03-01");
|
|
616
540
|
});
|
|
617
541
|
});
|
|
542
|
+
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// extractUserTimeZoneFromRecall
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
describe("extractUserTimeZoneFromRecall", () => {
|
|
548
|
+
test("returns null for empty input", () => {
|
|
549
|
+
expect(extractUserTimeZoneFromRecall("")).toBeNull();
|
|
550
|
+
expect(extractUserTimeZoneFromRecall(" ")).toBeNull();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("extracts IANA timezone from user_identity section", () => {
|
|
554
|
+
const text = `<memory_context>
|
|
555
|
+
|
|
556
|
+
<user_identity>
|
|
557
|
+
User's timezone is America/New_York
|
|
558
|
+
User works as a software engineer
|
|
559
|
+
</user_identity>
|
|
560
|
+
|
|
561
|
+
</memory_context>`;
|
|
562
|
+
expect(extractUserTimeZoneFromRecall(text)).toBe("America/New_York");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("extracts timezone from 'timezone: ...' line in identity", () => {
|
|
566
|
+
const text = `<memory_context>
|
|
567
|
+
|
|
568
|
+
<user_identity>
|
|
569
|
+
- name: Alice
|
|
570
|
+
- timezone: Europe/London
|
|
571
|
+
- role: designer
|
|
572
|
+
</user_identity>
|
|
573
|
+
|
|
574
|
+
</memory_context>`;
|
|
575
|
+
expect(extractUserTimeZoneFromRecall(text)).toBe("Europe/London");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("extracts UTC offset timezone", () => {
|
|
579
|
+
const text = `<memory_context>
|
|
580
|
+
|
|
581
|
+
<user_identity>
|
|
582
|
+
User's time zone is UTC+5:30
|
|
583
|
+
</user_identity>
|
|
584
|
+
|
|
585
|
+
</memory_context>`;
|
|
586
|
+
const result = extractUserTimeZoneFromRecall(text);
|
|
587
|
+
expect(result).not.toBeNull();
|
|
588
|
+
// UTC+5:30 should canonicalize to +05:30
|
|
589
|
+
expect(result).toBe("+05:30");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("falls back to scanning full text when no identity section", () => {
|
|
593
|
+
const text = `<memory_context>
|
|
594
|
+
|
|
595
|
+
<relevant_context>
|
|
596
|
+
<episode source="Mar 5">
|
|
597
|
+
User mentioned their timezone is Asia/Tokyo
|
|
598
|
+
</episode>
|
|
599
|
+
</relevant_context>
|
|
600
|
+
|
|
601
|
+
</memory_context>`;
|
|
602
|
+
expect(extractUserTimeZoneFromRecall(text)).toBe("Asia/Tokyo");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("returns null when no timezone info present", () => {
|
|
606
|
+
const text = `<memory_context>
|
|
607
|
+
|
|
608
|
+
<user_identity>
|
|
609
|
+
User's name is Bob
|
|
610
|
+
User works at Acme Corp
|
|
611
|
+
</user_identity>
|
|
612
|
+
|
|
613
|
+
</memory_context>`;
|
|
614
|
+
expect(extractUserTimeZoneFromRecall(text)).toBeNull();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("prefers identity section over other sections", () => {
|
|
618
|
+
const text = `<memory_context>
|
|
619
|
+
|
|
620
|
+
<user_identity>
|
|
621
|
+
User's timezone is America/Chicago
|
|
622
|
+
</user_identity>
|
|
623
|
+
|
|
624
|
+
<relevant_context>
|
|
625
|
+
<episode source="Mar 5">
|
|
626
|
+
Discussed timezone America/Los_Angeles for the deployment
|
|
627
|
+
</episode>
|
|
628
|
+
</relevant_context>
|
|
629
|
+
|
|
630
|
+
</memory_context>`;
|
|
631
|
+
expect(extractUserTimeZoneFromRecall(text)).toBe("America/Chicago");
|
|
632
|
+
});
|
|
633
|
+
});
|
|
@@ -399,4 +399,68 @@ describe("Verification control messages are deterministic (guard)", () => {
|
|
|
399
399
|
globalThis.fetch = originalFetch;
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
test("handleChannelInbound does not allow blocked members to bootstrap with /start gv_<token>", async () => {
|
|
404
|
+
const { createHash, randomBytes } = await import("node:crypto");
|
|
405
|
+
const { handleChannelInbound } =
|
|
406
|
+
await import("../runtime/routes/inbound-message-handler.js");
|
|
407
|
+
const { createOutboundSession } =
|
|
408
|
+
await import("../runtime/channel-verification-service.js");
|
|
409
|
+
const { upsertContactChannel } =
|
|
410
|
+
await import("../contacts/contacts-write.js");
|
|
411
|
+
|
|
412
|
+
const blockedIdentity = {
|
|
413
|
+
sourceChannel: "telegram",
|
|
414
|
+
externalUserId: "user-blocked-bootstrap",
|
|
415
|
+
externalChatId: "chat-blocked-bootstrap",
|
|
416
|
+
displayName: "Blocked Bootstrap User",
|
|
417
|
+
status: "blocked",
|
|
418
|
+
policy: "deny",
|
|
419
|
+
} as const;
|
|
420
|
+
upsertContactChannel(blockedIdentity);
|
|
421
|
+
|
|
422
|
+
const bootstrapToken = randomBytes(16).toString("hex");
|
|
423
|
+
const bootstrapTokenHash = createHash("sha256")
|
|
424
|
+
.update(bootstrapToken)
|
|
425
|
+
.digest("hex");
|
|
426
|
+
|
|
427
|
+
createOutboundSession({
|
|
428
|
+
channel: "telegram",
|
|
429
|
+
identityBindingStatus: "pending_bootstrap",
|
|
430
|
+
destinationAddress: blockedIdentity.externalUserId,
|
|
431
|
+
bootstrapTokenHash,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let processMessageCalled = false;
|
|
435
|
+
const processMessage = async () => {
|
|
436
|
+
processMessageCalled = true;
|
|
437
|
+
return { messageId: "msg-1" };
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const req = new Request("http://localhost/channels/inbound", {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { "Content-Type": "application/json" },
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
sourceChannel: "telegram",
|
|
445
|
+
interface: "telegram",
|
|
446
|
+
conversationExternalId: blockedIdentity.externalChatId,
|
|
447
|
+
externalMessageId: `msg-blocked-bootstrap-${Date.now()}`,
|
|
448
|
+
content: `/start gv_${bootstrapToken}`,
|
|
449
|
+
actorExternalId: blockedIdentity.externalUserId,
|
|
450
|
+
actorDisplayName: blockedIdentity.displayName,
|
|
451
|
+
sourceMetadata: {
|
|
452
|
+
commandIntent: { type: "start", payload: `gv_${bootstrapToken}` },
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const response = await handleChannelInbound(req, processMessage);
|
|
458
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
459
|
+
|
|
460
|
+
expect(body.accepted).toBe(true);
|
|
461
|
+
expect(body.denied).toBe(true);
|
|
462
|
+
expect(body.reason).toBe("member_blocked");
|
|
463
|
+
expect(body.verificationOutcome).toBeUndefined();
|
|
464
|
+
expect(processMessageCalled).toBe(false);
|
|
465
|
+
});
|
|
402
466
|
});
|
|
@@ -1427,3 +1427,96 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
|
|
|
1427
1427
|
expect(resolved!.status).toBe("approved");
|
|
1428
1428
|
});
|
|
1429
1429
|
});
|
|
1430
|
+
|
|
1431
|
+
// ===========================================================================
|
|
1432
|
+
// SECTION 11: Expired requests are excluded from routing
|
|
1433
|
+
// ===========================================================================
|
|
1434
|
+
|
|
1435
|
+
describe("routing invariant: expired requests are excluded from pending discovery", () => {
|
|
1436
|
+
beforeEach(() => resetTables());
|
|
1437
|
+
|
|
1438
|
+
test("expired request with hinted IDs is excluded from disambiguation", async () => {
|
|
1439
|
+
const expired = createCanonicalGuardianRequest({
|
|
1440
|
+
kind: "tool_approval",
|
|
1441
|
+
sourceType: "channel",
|
|
1442
|
+
conversationId: "conv-1",
|
|
1443
|
+
guardianExternalUserId: "guardian-1",
|
|
1444
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1445
|
+
requestCode: "EXP001",
|
|
1446
|
+
toolName: "shell",
|
|
1447
|
+
expiresAt: new Date(Date.now() - 10_000).toISOString(),
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
const active = createCanonicalGuardianRequest({
|
|
1451
|
+
kind: "tool_approval",
|
|
1452
|
+
sourceType: "channel",
|
|
1453
|
+
conversationId: "conv-1",
|
|
1454
|
+
guardianExternalUserId: "guardian-1",
|
|
1455
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1456
|
+
requestCode: "ACT001",
|
|
1457
|
+
toolName: "file_write",
|
|
1458
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1459
|
+
});
|
|
1460
|
+
registerPendingToolApprovalInteraction(active.id, "conv-1", "file_write");
|
|
1461
|
+
|
|
1462
|
+
// Both IDs are hinted but only the active one should be considered
|
|
1463
|
+
const result = await routeGuardianReply(
|
|
1464
|
+
replyCtx({
|
|
1465
|
+
messageText: "approve",
|
|
1466
|
+
conversationId: "conv-guardian-thread",
|
|
1467
|
+
pendingRequestIds: [expired.id, active.id],
|
|
1468
|
+
approvalConversationGenerator: undefined,
|
|
1469
|
+
}),
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1472
|
+
// Single active request — should apply directly, no disambiguation
|
|
1473
|
+
expect(result.consumed).toBe(true);
|
|
1474
|
+
expect(result.type).toBe("canonical_decision_applied");
|
|
1475
|
+
expect(result.decisionApplied).toBe(true);
|
|
1476
|
+
|
|
1477
|
+
const resolvedActive = getCanonicalGuardianRequest(active.id);
|
|
1478
|
+
expect(resolvedActive!.status).toBe("approved");
|
|
1479
|
+
|
|
1480
|
+
// Expired request untouched
|
|
1481
|
+
const resolvedExpired = getCanonicalGuardianRequest(expired.id);
|
|
1482
|
+
expect(resolvedExpired!.status).toBe("pending");
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
test("all expired hinted requests means no pending found — not consumed", async () => {
|
|
1486
|
+
const expired1 = createCanonicalGuardianRequest({
|
|
1487
|
+
kind: "tool_approval",
|
|
1488
|
+
sourceType: "channel",
|
|
1489
|
+
conversationId: "conv-1",
|
|
1490
|
+
guardianExternalUserId: "guardian-1",
|
|
1491
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1492
|
+
requestCode: "EXP002",
|
|
1493
|
+
toolName: "shell",
|
|
1494
|
+
expiresAt: new Date(Date.now() - 10_000).toISOString(),
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
const expired2 = createCanonicalGuardianRequest({
|
|
1498
|
+
kind: "tool_approval",
|
|
1499
|
+
sourceType: "channel",
|
|
1500
|
+
conversationId: "conv-1",
|
|
1501
|
+
guardianExternalUserId: "guardian-1",
|
|
1502
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1503
|
+
requestCode: "EXP003",
|
|
1504
|
+
toolName: "file_write",
|
|
1505
|
+
expiresAt: new Date(Date.now() - 5_000).toISOString(),
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const result = await routeGuardianReply(
|
|
1509
|
+
replyCtx({
|
|
1510
|
+
messageText: "approve",
|
|
1511
|
+
conversationId: "conv-guardian-thread",
|
|
1512
|
+
pendingRequestIds: [expired1.id, expired2.id],
|
|
1513
|
+
approvalConversationGenerator: undefined,
|
|
1514
|
+
}),
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
// No active pending requests — falls through
|
|
1518
|
+
expect(result.consumed).toBe(false);
|
|
1519
|
+
expect(result.type).toBe("not_consumed");
|
|
1520
|
+
expect(result.decisionApplied).toBe(false);
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
@@ -428,6 +428,251 @@ describe("repairHistory", () => {
|
|
|
428
428
|
expect(stats.orphanToolResultsDowngraded).toBe(0);
|
|
429
429
|
expect(stats.consecutiveSameRoleMerged).toBe(0);
|
|
430
430
|
});
|
|
431
|
+
|
|
432
|
+
test("keeps server_tool_use + web_search_tool_result paired in assistant message", () => {
|
|
433
|
+
// Both blocks should stay in the assistant message (self-paired)
|
|
434
|
+
const messages: Message[] = [
|
|
435
|
+
{ role: "user", content: [{ type: "text", text: "Search for cats" }] },
|
|
436
|
+
{
|
|
437
|
+
role: "assistant",
|
|
438
|
+
content: [
|
|
439
|
+
{
|
|
440
|
+
type: "server_tool_use",
|
|
441
|
+
id: "stu_1",
|
|
442
|
+
name: "web_search",
|
|
443
|
+
input: { query: "cats" },
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
type: "web_search_tool_result",
|
|
447
|
+
tool_use_id: "stu_1",
|
|
448
|
+
content: [{ type: "web_search_result", url: "https://cats.com" }],
|
|
449
|
+
},
|
|
450
|
+
{ type: "text", text: "Here are some results about cats." },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
456
|
+
|
|
457
|
+
expect(repaired).toEqual(messages);
|
|
458
|
+
expect(stats.missingToolResultsInserted).toBe(0);
|
|
459
|
+
expect(stats.orphanToolResultsDowngraded).toBe(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("synthesizes web_search_tool_result in assistant message when missing (interrupted stream)", () => {
|
|
463
|
+
// If server_tool_use has no paired result (e.g. stream was interrupted),
|
|
464
|
+
// the synthetic result goes in the SAME assistant message, not a user message
|
|
465
|
+
const messages: Message[] = [
|
|
466
|
+
{ role: "user", content: [{ type: "text", text: "Search" }] },
|
|
467
|
+
{
|
|
468
|
+
role: "assistant",
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: "server_tool_use",
|
|
472
|
+
id: "stu_1",
|
|
473
|
+
name: "web_search",
|
|
474
|
+
input: { query: "test" },
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
role: "user",
|
|
480
|
+
content: [{ type: "text", text: "next message" }],
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
485
|
+
|
|
486
|
+
expect(stats.missingToolResultsInserted).toBe(1);
|
|
487
|
+
|
|
488
|
+
// Synthetic result is in the assistant message, not the user message
|
|
489
|
+
const assistantMsg = repaired[1];
|
|
490
|
+
expect(assistantMsg.content).toHaveLength(2);
|
|
491
|
+
expect(assistantMsg.content[1]).toMatchObject({
|
|
492
|
+
type: "web_search_tool_result",
|
|
493
|
+
tool_use_id: "stu_1",
|
|
494
|
+
content: { type: "web_search_tool_result_error", error_code: "unavailable" },
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// User message has no web_search_tool_result
|
|
498
|
+
const userMsg = repaired[2];
|
|
499
|
+
expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("migrates legacy web_search_tool_result from user message to assistant message", () => {
|
|
503
|
+
// Old history format: server_tool_use in assistant, web_search_tool_result in user.
|
|
504
|
+
// Repair: synthesize result in assistant, orphan-downgrade the user-side result.
|
|
505
|
+
const messages: Message[] = [
|
|
506
|
+
{ role: "user", content: [{ type: "text", text: "Search" }] },
|
|
507
|
+
{
|
|
508
|
+
role: "assistant",
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: "server_tool_use",
|
|
512
|
+
id: "srvtoolu_abc",
|
|
513
|
+
name: "web_search",
|
|
514
|
+
input: { query: "test" },
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
type: "tool_use",
|
|
518
|
+
id: "tu_1",
|
|
519
|
+
name: "bash",
|
|
520
|
+
input: { cmd: "ls" },
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
role: "user",
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "web_search_tool_result",
|
|
529
|
+
tool_use_id: "srvtoolu_abc",
|
|
530
|
+
content: [{ type: "web_search_result", url: "https://example.com" }],
|
|
531
|
+
},
|
|
532
|
+
{ type: "tool_result", tool_use_id: "tu_1", content: "files" },
|
|
533
|
+
],
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
role: "assistant",
|
|
537
|
+
content: [{ type: "text", text: "Done" }],
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
542
|
+
|
|
543
|
+
// Synthetic web_search_tool_result added to assistant message
|
|
544
|
+
expect(stats.missingToolResultsInserted).toBe(1);
|
|
545
|
+
|
|
546
|
+
// The assistant message now has the server pair + client tool_use
|
|
547
|
+
const assistantMsg = repaired[1];
|
|
548
|
+
const serverToolUse = assistantMsg.content.find((b) => b.type === "server_tool_use");
|
|
549
|
+
const webSearchResult = assistantMsg.content.find((b) => b.type === "web_search_tool_result");
|
|
550
|
+
expect(serverToolUse).toBeDefined();
|
|
551
|
+
expect(webSearchResult).toBeDefined();
|
|
552
|
+
|
|
553
|
+
// The user message has tool_result for tu_1, and the old web_search_tool_result is downgraded
|
|
554
|
+
const userMsg = repaired[2];
|
|
555
|
+
expect(stats.orphanToolResultsDowngraded).toBe(1);
|
|
556
|
+
expect(userMsg.content.some((b) => b.type === "tool_result")).toBe(true);
|
|
557
|
+
expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("trailing server_tool_use gets synthetic result in same assistant message", () => {
|
|
561
|
+
// No trailing user message needed — result goes in the assistant message
|
|
562
|
+
const messages: Message[] = [
|
|
563
|
+
{ role: "user", content: [{ type: "text", text: "Go" }] },
|
|
564
|
+
{
|
|
565
|
+
role: "assistant",
|
|
566
|
+
content: [
|
|
567
|
+
{
|
|
568
|
+
type: "server_tool_use",
|
|
569
|
+
id: "stu_1",
|
|
570
|
+
name: "web_search",
|
|
571
|
+
input: { query: "test" },
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
578
|
+
|
|
579
|
+
expect(stats.missingToolResultsInserted).toBe(1);
|
|
580
|
+
// Result is in the assistant message
|
|
581
|
+
expect(repaired).toHaveLength(2);
|
|
582
|
+
expect(repaired[1].role).toBe("assistant");
|
|
583
|
+
expect(repaired[1].content).toHaveLength(2);
|
|
584
|
+
expect(repaired[1].content[1]).toMatchObject({
|
|
585
|
+
type: "web_search_tool_result",
|
|
586
|
+
tool_use_id: "stu_1",
|
|
587
|
+
content: { type: "web_search_tool_result_error", error_code: "unavailable" },
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("downgrades type-mismatched tool_result for server_tool_use", () => {
|
|
592
|
+
// A tool_result in the user message for a server_tool_use ID is orphaned —
|
|
593
|
+
// server-side results belong in the assistant message
|
|
594
|
+
const messages: Message[] = [
|
|
595
|
+
{ role: "user", content: [{ type: "text", text: "Search" }] },
|
|
596
|
+
{
|
|
597
|
+
role: "assistant",
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: "server_tool_use",
|
|
601
|
+
id: "stu_1",
|
|
602
|
+
name: "web_search",
|
|
603
|
+
input: { query: "test" },
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
role: "user",
|
|
609
|
+
content: [
|
|
610
|
+
{ type: "tool_result", tool_use_id: "stu_1", content: "wrong type" },
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
role: "assistant",
|
|
615
|
+
content: [{ type: "text", text: "Done" }],
|
|
616
|
+
},
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
620
|
+
|
|
621
|
+
// Synthetic web_search_tool_result added to assistant message
|
|
622
|
+
expect(stats.missingToolResultsInserted).toBe(1);
|
|
623
|
+
// The mismatched tool_result in user message is orphaned (no pending client tool_use)
|
|
624
|
+
expect(stats.orphanToolResultsDowngraded).toBe(1);
|
|
625
|
+
|
|
626
|
+
// Assistant message has the server pair
|
|
627
|
+
const assistantMsg = repaired[1];
|
|
628
|
+
expect(assistantMsg.content.some((b) => b.type === "web_search_tool_result")).toBe(true);
|
|
629
|
+
|
|
630
|
+
// User message has no web_search_tool_result — the tool_result was downgraded to text
|
|
631
|
+
const userMsg = repaired[2];
|
|
632
|
+
expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
|
|
633
|
+
expect(userMsg.content.every((b) => b.type !== "tool_result")).toBe(true);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("downgrades type-mismatched web_search_tool_result for tool_use", () => {
|
|
637
|
+
// A web_search_tool_result paired with a regular tool_use ID is a type mismatch
|
|
638
|
+
const messages: Message[] = [
|
|
639
|
+
{ role: "user", content: [{ type: "text", text: "Go" }] },
|
|
640
|
+
{
|
|
641
|
+
role: "assistant",
|
|
642
|
+
content: [
|
|
643
|
+
{ type: "tool_use", id: "tu_1", name: "bash", input: { cmd: "ls" } },
|
|
644
|
+
],
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
role: "user",
|
|
648
|
+
content: [
|
|
649
|
+
{
|
|
650
|
+
type: "web_search_tool_result",
|
|
651
|
+
tool_use_id: "tu_1",
|
|
652
|
+
content: [{ type: "web_search_result", url: "https://example.com" }],
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
role: "assistant",
|
|
658
|
+
content: [{ type: "text", text: "Done" }],
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
const { messages: repaired, stats } = repairHistory(messages);
|
|
663
|
+
|
|
664
|
+
expect(stats.orphanToolResultsDowngraded).toBe(1);
|
|
665
|
+
expect(stats.missingToolResultsInserted).toBe(1);
|
|
666
|
+
|
|
667
|
+
const userMsg = repaired[2];
|
|
668
|
+
const trBlocks = userMsg.content.filter((b) => b.type === "tool_result");
|
|
669
|
+
expect(trBlocks).toHaveLength(1);
|
|
670
|
+
expect(trBlocks[0]).toMatchObject({
|
|
671
|
+
type: "tool_result",
|
|
672
|
+
tool_use_id: "tu_1",
|
|
673
|
+
is_error: true,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
431
676
|
});
|
|
432
677
|
|
|
433
678
|
describe("deepRepairHistory", () => {
|