@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
|
@@ -1426,9 +1426,156 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
|
|
|
1426
1426
|
|
|
1427
1427
|
expect(config.llm.profiles.balanced?.status).toBe("disabled");
|
|
1428
1428
|
expect(config.llm.profiles["quality-optimized"]?.status).toBe("disabled");
|
|
1429
|
+
expect(config.llm.profiles.frontier?.status).toBe("disabled");
|
|
1429
1430
|
expect(config.llm.profiles["cost-optimized"]?.status).toBe("disabled");
|
|
1430
1431
|
});
|
|
1431
1432
|
|
|
1433
|
+
test("off-platform BYOK hatch defaults advisor to the personal quality profile", () => {
|
|
1434
|
+
const overlayPath = join(WORKSPACE_DIR, "hatch-overlay.json");
|
|
1435
|
+
writeFileSync(
|
|
1436
|
+
overlayPath,
|
|
1437
|
+
JSON.stringify({ llm: { default: { provider: "anthropic" } } }, null, 2) +
|
|
1438
|
+
"\n",
|
|
1439
|
+
);
|
|
1440
|
+
process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH = overlayPath;
|
|
1441
|
+
|
|
1442
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
1443
|
+
const config = loadConfig();
|
|
1444
|
+
|
|
1445
|
+
expect(config.llm.activeProfile).toBe("custom-balanced");
|
|
1446
|
+
expect(config.llm.advisorProfile).toBe("custom-quality-optimized");
|
|
1447
|
+
expect(config.llm.profiles["custom-quality-optimized"]?.provider).toBe(
|
|
1448
|
+
"anthropic",
|
|
1449
|
+
);
|
|
1450
|
+
expect(
|
|
1451
|
+
config.llm.profiles["custom-quality-optimized"]?.provider_connection,
|
|
1452
|
+
).toBe("anthropic-personal");
|
|
1453
|
+
expect(config.llm.profiles.frontier?.status).toBe("disabled");
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
test("off-platform boot repairs a disabled managed advisor to a personal profile when no active managed replacement exists", () => {
|
|
1457
|
+
writeConfig({
|
|
1458
|
+
llm: {
|
|
1459
|
+
advisorProfile: "frontier",
|
|
1460
|
+
profiles: {
|
|
1461
|
+
frontier: {
|
|
1462
|
+
source: "managed",
|
|
1463
|
+
provider: "anthropic",
|
|
1464
|
+
provider_connection: "anthropic-managed",
|
|
1465
|
+
model: "claude-opus-4-8",
|
|
1466
|
+
status: "disabled",
|
|
1467
|
+
},
|
|
1468
|
+
balanced: {
|
|
1469
|
+
source: "managed",
|
|
1470
|
+
provider: "together",
|
|
1471
|
+
provider_connection: "together-managed",
|
|
1472
|
+
model: "open-model",
|
|
1473
|
+
status: "disabled",
|
|
1474
|
+
},
|
|
1475
|
+
"quality-optimized": {
|
|
1476
|
+
source: "managed",
|
|
1477
|
+
provider: "fireworks",
|
|
1478
|
+
provider_connection: "fireworks-managed",
|
|
1479
|
+
model: "accounts/fireworks/models/glm-5p2",
|
|
1480
|
+
status: "disabled",
|
|
1481
|
+
},
|
|
1482
|
+
"cost-optimized": {
|
|
1483
|
+
source: "managed",
|
|
1484
|
+
provider: "fireworks",
|
|
1485
|
+
provider_connection: "fireworks-managed",
|
|
1486
|
+
model: "accounts/fireworks/models/deepseek-v4-flash",
|
|
1487
|
+
status: "disabled",
|
|
1488
|
+
},
|
|
1489
|
+
"custom-quality-optimized": {
|
|
1490
|
+
source: "user",
|
|
1491
|
+
provider: "anthropic",
|
|
1492
|
+
provider_connection: "anthropic-personal",
|
|
1493
|
+
model: "claude-opus-4-8",
|
|
1494
|
+
label: "Quality",
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
1501
|
+
const config = loadConfig();
|
|
1502
|
+
|
|
1503
|
+
expect(config.llm.advisorProfile).toBe("custom-quality-optimized");
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
test("platform boot repairs a disabled managed advisor to an active managed profile", () => {
|
|
1507
|
+
process.env.IS_PLATFORM = "true";
|
|
1508
|
+
writeConfig({
|
|
1509
|
+
llm: {
|
|
1510
|
+
advisorProfile: "frontier",
|
|
1511
|
+
profiles: {
|
|
1512
|
+
frontier: {
|
|
1513
|
+
source: "managed",
|
|
1514
|
+
provider: "anthropic",
|
|
1515
|
+
provider_connection: "anthropic-managed",
|
|
1516
|
+
model: "claude-opus-4-8",
|
|
1517
|
+
status: "disabled",
|
|
1518
|
+
},
|
|
1519
|
+
"custom-quality-optimized": {
|
|
1520
|
+
source: "user",
|
|
1521
|
+
provider: "anthropic",
|
|
1522
|
+
provider_connection: "anthropic-personal",
|
|
1523
|
+
model: "claude-opus-4-8",
|
|
1524
|
+
label: "Quality",
|
|
1525
|
+
},
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
1531
|
+
const config = loadConfig();
|
|
1532
|
+
|
|
1533
|
+
expect(config.llm.advisorProfile).toBe("quality-optimized");
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
test("off-platform boot clears a disabled managed advisor when no active replacement exists", () => {
|
|
1537
|
+
writeConfig({
|
|
1538
|
+
llm: {
|
|
1539
|
+
advisorProfile: "frontier",
|
|
1540
|
+
profiles: {
|
|
1541
|
+
frontier: {
|
|
1542
|
+
source: "managed",
|
|
1543
|
+
provider: "anthropic",
|
|
1544
|
+
provider_connection: "anthropic-managed",
|
|
1545
|
+
model: "claude-opus-4-8",
|
|
1546
|
+
status: "disabled",
|
|
1547
|
+
},
|
|
1548
|
+
balanced: {
|
|
1549
|
+
source: "managed",
|
|
1550
|
+
provider: "together",
|
|
1551
|
+
provider_connection: "together-managed",
|
|
1552
|
+
model: "open-model",
|
|
1553
|
+
status: "disabled",
|
|
1554
|
+
},
|
|
1555
|
+
"quality-optimized": {
|
|
1556
|
+
source: "managed",
|
|
1557
|
+
provider: "fireworks",
|
|
1558
|
+
provider_connection: "fireworks-managed",
|
|
1559
|
+
model: "accounts/fireworks/models/glm-5p2",
|
|
1560
|
+
status: "disabled",
|
|
1561
|
+
},
|
|
1562
|
+
"cost-optimized": {
|
|
1563
|
+
source: "managed",
|
|
1564
|
+
provider: "fireworks",
|
|
1565
|
+
provider_connection: "fireworks-managed",
|
|
1566
|
+
model: "accounts/fireworks/models/deepseek-v4-flash",
|
|
1567
|
+
status: "disabled",
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
1574
|
+
const config = loadConfig();
|
|
1575
|
+
|
|
1576
|
+
expect(config.llm.advisorProfile).toBeUndefined();
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1432
1579
|
test("off-platform managed-inference hatch keeps selected managed connection active", () => {
|
|
1433
1580
|
const overlayPath = join(WORKSPACE_DIR, "hatch-overlay.json");
|
|
1434
1581
|
writeFileSync(
|
|
@@ -1451,6 +1598,7 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
|
|
|
1451
1598
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1452
1599
|
|
|
1453
1600
|
expect(raw.llm.activeProfile).toBe("balanced");
|
|
1601
|
+
expect(raw.llm.advisorProfile).toBe("balanced");
|
|
1454
1602
|
expect(raw.llm.profiles.balanced.provider_connection).toBe(
|
|
1455
1603
|
"together-managed",
|
|
1456
1604
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `createConsultDeadline` — the advisor consult's progress-aware
|
|
3
|
+
* timeout. Uses short real timers with generous (>=3x) margins so a streamed
|
|
4
|
+
* chunk reliably lands inside the idle window without timing flakiness.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import { createConsultDeadline } from "../tools/subagent/consult-deadline.js";
|
|
9
|
+
|
|
10
|
+
const delay = (ms: number): Promise<void> =>
|
|
11
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
describe("createConsultDeadline", () => {
|
|
14
|
+
test("streamed progress keeps the consult alive past the idle window", async () => {
|
|
15
|
+
// Record progress every 25ms against a 150ms idle window (6x margin). Each
|
|
16
|
+
// chunk resets the window, so it must never abort over ~200ms of streaming.
|
|
17
|
+
const d = createConsultDeadline({ idleMs: 150, maxMs: 10_000 });
|
|
18
|
+
try {
|
|
19
|
+
for (let i = 0; i < 8; i++) {
|
|
20
|
+
await delay(25);
|
|
21
|
+
expect(d.signal.aborted).toBe(false);
|
|
22
|
+
d.recordProgress();
|
|
23
|
+
}
|
|
24
|
+
} finally {
|
|
25
|
+
d.dispose();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("aborts after the idle window once progress stops", async () => {
|
|
30
|
+
const d = createConsultDeadline({ idleMs: 50, maxMs: 10_000 });
|
|
31
|
+
try {
|
|
32
|
+
expect(d.signal.aborted).toBe(false);
|
|
33
|
+
await delay(250); // 5x the idle window with no progress
|
|
34
|
+
expect(d.signal.aborted).toBe(true);
|
|
35
|
+
} finally {
|
|
36
|
+
d.dispose();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("aborts at the absolute max even under steady progress", async () => {
|
|
41
|
+
// Idle window can't fire (huge); the short max must, despite steady chunks.
|
|
42
|
+
const d = createConsultDeadline({ idleMs: 10_000, maxMs: 70 });
|
|
43
|
+
try {
|
|
44
|
+
for (let i = 0; i < 8; i++) {
|
|
45
|
+
await delay(25);
|
|
46
|
+
d.recordProgress();
|
|
47
|
+
}
|
|
48
|
+
expect(d.signal.aborted).toBe(true); // ~200ms elapsed > 70ms max
|
|
49
|
+
} finally {
|
|
50
|
+
d.dispose();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("dispose() cancels both timers — no late abort", async () => {
|
|
55
|
+
const d = createConsultDeadline({ idleMs: 40, maxMs: 40 });
|
|
56
|
+
d.dispose();
|
|
57
|
+
await delay(150); // well past both windows
|
|
58
|
+
expect(d.signal.aborted).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INFO telemetry (interactionCount/lastInteraction/lastSeenAt) is local, not
|
|
3
|
+
* ACL: the gateway's handle-inbound mirror writes it to the assistant DB, and
|
|
4
|
+
* model-facing turn context reads it back. These tests assert the daemon-native
|
|
5
|
+
* read paths (getContact / searchContacts) re-hydrate and aggregate it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
mock.module("../util/logger.js", () => ({
|
|
11
|
+
getLogger: () =>
|
|
12
|
+
new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
findContactInfoById,
|
|
19
|
+
getContact,
|
|
20
|
+
searchContacts,
|
|
21
|
+
} from "../contacts/contact-store.js";
|
|
22
|
+
import { getSqlite } from "../memory/db-connection.js";
|
|
23
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
24
|
+
|
|
25
|
+
await initializeDb();
|
|
26
|
+
|
|
27
|
+
function resetContactTables(): void {
|
|
28
|
+
const sqlite = getSqlite();
|
|
29
|
+
sqlite.run("DELETE FROM contact_channels");
|
|
30
|
+
sqlite.run("DELETE FROM contacts");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function insertContact(id: string, displayName: string): void {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
getSqlite().run(
|
|
36
|
+
"INSERT INTO contacts (id, display_name, role, contact_type, user_file, created_at, updated_at) VALUES (?, ?, 'contact', 'human', ?, ?, ?)",
|
|
37
|
+
[id, displayName, `${id}.md`, now, now],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function insertChannel(params: {
|
|
42
|
+
id: string;
|
|
43
|
+
contactId: string;
|
|
44
|
+
type: string;
|
|
45
|
+
address: string;
|
|
46
|
+
interactionCount: number;
|
|
47
|
+
lastInteraction: number | null;
|
|
48
|
+
lastSeenAt?: number | null;
|
|
49
|
+
}): void {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
getSqlite().run(
|
|
52
|
+
"INSERT INTO contact_channels (id, contact_id, type, address, is_primary, interaction_count, last_interaction, last_seen_at, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?)",
|
|
53
|
+
[
|
|
54
|
+
params.id,
|
|
55
|
+
params.contactId,
|
|
56
|
+
params.type,
|
|
57
|
+
params.address,
|
|
58
|
+
params.interactionCount,
|
|
59
|
+
params.lastInteraction,
|
|
60
|
+
params.lastSeenAt ?? null,
|
|
61
|
+
now,
|
|
62
|
+
now,
|
|
63
|
+
],
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("contact interaction INFO aggregation", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
resetContactTables();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("getContact sums interaction_count and takes the latest last_interaction across channels", () => {
|
|
73
|
+
insertContact("ct_1", "Alice");
|
|
74
|
+
insertChannel({
|
|
75
|
+
id: "ch_a",
|
|
76
|
+
contactId: "ct_1",
|
|
77
|
+
type: "phone",
|
|
78
|
+
address: "+15550100",
|
|
79
|
+
interactionCount: 3,
|
|
80
|
+
lastInteraction: 1900,
|
|
81
|
+
lastSeenAt: 1850,
|
|
82
|
+
});
|
|
83
|
+
insertChannel({
|
|
84
|
+
id: "ch_b",
|
|
85
|
+
contactId: "ct_1",
|
|
86
|
+
type: "email",
|
|
87
|
+
address: "alice@example.com",
|
|
88
|
+
interactionCount: 4,
|
|
89
|
+
lastInteraction: 2100,
|
|
90
|
+
lastSeenAt: 2050,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const contact = getContact("ct_1");
|
|
94
|
+
expect(contact).not.toBeNull();
|
|
95
|
+
expect(contact!.interactionCount).toBe(7);
|
|
96
|
+
expect(contact!.lastInteraction).toBe(2100);
|
|
97
|
+
|
|
98
|
+
// Per-channel INFO is hydrated too.
|
|
99
|
+
const phone = contact!.channels.find((c) => c.type === "phone");
|
|
100
|
+
expect(phone?.interactionCount).toBe(3);
|
|
101
|
+
expect(phone?.lastInteraction).toBe(1900);
|
|
102
|
+
expect(phone?.lastSeenAt).toBe(1850);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("findContactInfoById surfaces the real interaction count", () => {
|
|
106
|
+
insertContact("ct_2", "Bob");
|
|
107
|
+
insertChannel({
|
|
108
|
+
id: "ch_c",
|
|
109
|
+
contactId: "ct_2",
|
|
110
|
+
type: "phone",
|
|
111
|
+
address: "+15550200",
|
|
112
|
+
interactionCount: 5,
|
|
113
|
+
lastInteraction: 1000,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const info = findContactInfoById("ct_2");
|
|
117
|
+
expect(info?.interactionCount).toBe(5);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("lastInteraction is null when no channel has interacted", () => {
|
|
121
|
+
insertContact("ct_3", "Carol");
|
|
122
|
+
insertChannel({
|
|
123
|
+
id: "ch_d",
|
|
124
|
+
contactId: "ct_3",
|
|
125
|
+
type: "phone",
|
|
126
|
+
address: "+15550300",
|
|
127
|
+
interactionCount: 0,
|
|
128
|
+
lastInteraction: null,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const contact = getContact("ct_3");
|
|
132
|
+
expect(contact!.interactionCount).toBe(0);
|
|
133
|
+
expect(contact!.lastInteraction).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("filtered searchContacts carries channel INFO fields", () => {
|
|
137
|
+
insertContact("ct_4", "Dana");
|
|
138
|
+
insertChannel({
|
|
139
|
+
id: "ch_e",
|
|
140
|
+
contactId: "ct_4",
|
|
141
|
+
type: "phone",
|
|
142
|
+
address: "+15550400",
|
|
143
|
+
interactionCount: 9,
|
|
144
|
+
lastInteraction: 3000,
|
|
145
|
+
lastSeenAt: 2900,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const results = searchContacts({ query: "Dana", limit: 10 });
|
|
149
|
+
expect(results).toHaveLength(1);
|
|
150
|
+
expect(results[0].interactionCount).toBe(9);
|
|
151
|
+
expect(results[0].lastInteraction).toBe(3000);
|
|
152
|
+
const ch = results[0].channels[0];
|
|
153
|
+
expect(ch.interactionCount).toBe(9);
|
|
154
|
+
expect(ch.lastSeenAt).toBe(2900);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -66,11 +66,10 @@ describe("upsertContact user_file selection", () => {
|
|
|
66
66
|
resetContactTables();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test("
|
|
69
|
+
test("assigns a fresh slug per contact; principalId is gateway-owned and no longer groups siblings locally", () => {
|
|
70
70
|
const primary = upsertContact({
|
|
71
71
|
displayName: "Chris",
|
|
72
72
|
role: "guardian",
|
|
73
|
-
principalId: "principal-abc",
|
|
74
73
|
channels: [
|
|
75
74
|
{
|
|
76
75
|
type: "vellum",
|
|
@@ -80,12 +79,12 @@ describe("upsertContact user_file selection", () => {
|
|
|
80
79
|
});
|
|
81
80
|
expect(primary.userFile).toBe("chris.md");
|
|
82
81
|
|
|
83
|
-
//
|
|
84
|
-
// first
|
|
82
|
+
// A second contact with the same display name no longer inherits the
|
|
83
|
+
// first's userFile via a local principal lookup — it gets a fresh
|
|
84
|
+
// (collision-incremented) slug. Sibling grouping is owned by the gateway.
|
|
85
85
|
const slack = upsertContact({
|
|
86
86
|
displayName: "chris",
|
|
87
87
|
role: "guardian",
|
|
88
|
-
principalId: "principal-abc",
|
|
89
88
|
channels: [
|
|
90
89
|
{
|
|
91
90
|
type: "slack",
|
|
@@ -94,15 +93,14 @@ describe("upsertContact user_file selection", () => {
|
|
|
94
93
|
},
|
|
95
94
|
],
|
|
96
95
|
});
|
|
97
|
-
expect(slack.userFile).toBe("chris.md");
|
|
96
|
+
expect(slack.userFile).toBe("chris-2.md");
|
|
98
97
|
expect(slack.id).not.toBe(primary.id);
|
|
99
98
|
});
|
|
100
99
|
|
|
101
|
-
test("
|
|
100
|
+
test("generates a slug from the display name for a brand-new contact", () => {
|
|
102
101
|
const contact = upsertContact({
|
|
103
102
|
displayName: "Alice",
|
|
104
103
|
role: "contact",
|
|
105
|
-
principalId: "principal-alone",
|
|
106
104
|
channels: [
|
|
107
105
|
{
|
|
108
106
|
type: "slack",
|
|
@@ -141,7 +139,7 @@ describe("upsertContact user_file selection", () => {
|
|
|
141
139
|
expect(second.userFile).toBe("bob-2.md");
|
|
142
140
|
});
|
|
143
141
|
|
|
144
|
-
test("
|
|
142
|
+
test("a null-userFile contact does not block a fresh slug for a new contact", () => {
|
|
145
143
|
insertContact({
|
|
146
144
|
id: "seed-null",
|
|
147
145
|
displayName: "legacy",
|
|
@@ -154,7 +152,6 @@ describe("upsertContact user_file selection", () => {
|
|
|
154
152
|
const contact = upsertContact({
|
|
155
153
|
displayName: "Legacy",
|
|
156
154
|
role: "guardian",
|
|
157
|
-
principalId: "principal-null",
|
|
158
155
|
channels: [
|
|
159
156
|
{
|
|
160
157
|
type: "phone",
|
|
@@ -63,19 +63,18 @@ const realContactStore = await import("../contacts/contact-store.js");
|
|
|
63
63
|
// contactType is filtered in SQL (before the limit) rather than relayed.
|
|
64
64
|
const listContactsArgs: Array<{
|
|
65
65
|
limit?: number;
|
|
66
|
-
role?: string;
|
|
67
66
|
contactType?: string;
|
|
68
67
|
}> = [];
|
|
69
68
|
mock.module("../contacts/contact-store.js", () => ({
|
|
70
69
|
...realContactStore,
|
|
71
|
-
listContacts: (limit?: number,
|
|
70
|
+
listContacts: (limit?: number, contactType?: string) => {
|
|
72
71
|
localCalls.push("listContacts");
|
|
73
|
-
listContactsArgs.push({ limit,
|
|
72
|
+
listContactsArgs.push({ limit, contactType });
|
|
74
73
|
return [
|
|
75
74
|
{
|
|
76
75
|
id: "local-1",
|
|
77
76
|
displayName: "Local Contact",
|
|
78
|
-
role:
|
|
77
|
+
role: "contact",
|
|
79
78
|
contactType: contactType ?? "human",
|
|
80
79
|
interactionCount: 0,
|
|
81
80
|
channels: [],
|
|
@@ -248,20 +247,18 @@ describe("handleListContacts relay", () => {
|
|
|
248
247
|
// contactType + limit are pushed into the SQL-filtered daemon read (the
|
|
249
248
|
// daemon-native listContacts filters contactType BEFORE applying limit).
|
|
250
249
|
expect(listContactsArgs).toEqual([
|
|
251
|
-
{ limit: 50,
|
|
250
|
+
{ limit: 50, contactType: "assistant" },
|
|
252
251
|
]);
|
|
253
252
|
expect(result.contacts[0].id).toBe("local-1");
|
|
254
253
|
expect(result.contacts[0].contactType).toBe("assistant");
|
|
255
254
|
expect(debugLogs.some((m) => m.includes("daemon-native"))).toBe(true);
|
|
256
255
|
});
|
|
257
256
|
|
|
258
|
-
test("contactType
|
|
257
|
+
test("contactType filters daemon-native; role is gateway-owned and no longer a local predicate", async () => {
|
|
259
258
|
await handleListContacts({ contactType: "human", role: "guardian" });
|
|
260
259
|
|
|
261
260
|
expect(ipcCalls).toEqual([]);
|
|
262
|
-
expect(listContactsArgs).toEqual([
|
|
263
|
-
{ limit: 50, role: "guardian", contactType: "human" },
|
|
264
|
-
]);
|
|
261
|
+
expect(listContactsArgs).toEqual([{ limit: 50, contactType: "human" }]);
|
|
265
262
|
});
|
|
266
263
|
});
|
|
267
264
|
|
|
@@ -257,8 +257,8 @@ mock.module("../plugins/defaults/compaction/overflow-policy.js", () => ({
|
|
|
257
257
|
}));
|
|
258
258
|
|
|
259
259
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
setConversationProcessingStartedAt: () => {},
|
|
261
|
+
isConversationProcessing: () => false,
|
|
262
262
|
setConversationOriginChannelIfUnset: () => {},
|
|
263
263
|
setConversationHistoryStrippedAt: () => {},
|
|
264
264
|
updateConversationUsage: () => {},
|
|
@@ -289,6 +289,8 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
289
289
|
getLastUserTimestampBefore: () => 0,
|
|
290
290
|
resolveOverrideProfile: () => undefined,
|
|
291
291
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
292
|
+
recordConversationPersistedSeq: () => {},
|
|
293
|
+
getConversationPersistedSeq: () => null,
|
|
292
294
|
}));
|
|
293
295
|
|
|
294
296
|
afterAll(() => {
|
|
@@ -278,6 +278,7 @@ const deleteMessageByIdMock = mock(() => ({
|
|
|
278
278
|
}));
|
|
279
279
|
const reserveMessageMock = mock(async () => ({ id: "msg-reserve" }));
|
|
280
280
|
const updateMessageContentMock = mock(() => {});
|
|
281
|
+
const addMessageMock = mock(() => ({ id: "mock-msg-id" }));
|
|
281
282
|
mock.module("../memory/conversation-crud.js", () => ({
|
|
282
283
|
setConversationProcessingStartedAt: () => {},
|
|
283
284
|
isConversationProcessing: () => false,
|
|
@@ -292,7 +293,7 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
292
293
|
trustContext: undefined,
|
|
293
294
|
}),
|
|
294
295
|
getConversationOriginInterface: () => null,
|
|
295
|
-
addMessage:
|
|
296
|
+
addMessage: addMessageMock,
|
|
296
297
|
deleteMessageById: deleteMessageByIdMock,
|
|
297
298
|
updateConversationContextWindow: () => {},
|
|
298
299
|
updateConversationSlackContextWatermark:
|
|
@@ -303,6 +304,8 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
303
304
|
getLastUserTimestampBefore: () => 0,
|
|
304
305
|
reserveMessage: reserveMessageMock,
|
|
305
306
|
updateMessageContent: updateMessageContentMock,
|
|
307
|
+
recordConversationPersistedSeq: () => {},
|
|
308
|
+
getConversationPersistedSeq: () => null,
|
|
306
309
|
// The real schema is a Zod object; tests don't exercise validation,
|
|
307
310
|
// so a passthrough is sufficient — the production code at
|
|
308
311
|
// `handleMessageComplete` only branches on `success` and reads two
|
|
@@ -558,13 +561,16 @@ mock.module("../workspace/git-service.js", () => ({
|
|
|
558
561
|
}),
|
|
559
562
|
}));
|
|
560
563
|
|
|
564
|
+
let mockConversationErrorClassification = {
|
|
565
|
+
code: "CONVERSATION_PROCESSING_FAILED",
|
|
566
|
+
userMessage: "Something went wrong processing your message.",
|
|
567
|
+
retryable: false,
|
|
568
|
+
errorCategory: "processing_failed",
|
|
569
|
+
};
|
|
570
|
+
|
|
561
571
|
mock.module("../daemon/conversation-error.js", () => ({
|
|
562
|
-
classifyConversationError: (_err: unknown, _ctx: unknown) =>
|
|
563
|
-
|
|
564
|
-
userMessage: "Something went wrong processing your message.",
|
|
565
|
-
retryable: false,
|
|
566
|
-
errorCategory: "processing_failed",
|
|
567
|
-
}),
|
|
572
|
+
classifyConversationError: (_err: unknown, _ctx: unknown) =>
|
|
573
|
+
mockConversationErrorClassification,
|
|
568
574
|
isUserCancellation: (err: unknown, ctx: { aborted?: boolean }) => {
|
|
569
575
|
if (!ctx.aborted) return false;
|
|
570
576
|
if (err instanceof DOMException && err.name === "AbortError") return true;
|
|
@@ -870,6 +876,13 @@ beforeEach(() => {
|
|
|
870
876
|
deleteMessageByIdMock.mockClear();
|
|
871
877
|
reserveMessageMock.mockClear();
|
|
872
878
|
updateMessageContentMock.mockClear();
|
|
879
|
+
addMessageMock.mockClear();
|
|
880
|
+
mockConversationErrorClassification = {
|
|
881
|
+
code: "CONVERSATION_PROCESSING_FAILED",
|
|
882
|
+
userMessage: "Something went wrong processing your message.",
|
|
883
|
+
retryable: false,
|
|
884
|
+
errorCategory: "processing_failed",
|
|
885
|
+
};
|
|
873
886
|
indexMessageNowMock.mockClear();
|
|
874
887
|
projectAssistantMessageMock.mockClear();
|
|
875
888
|
publishSyncInvalidationMock.mockClear();
|
|
@@ -2239,6 +2252,49 @@ describe("session-agent-loop", () => {
|
|
|
2239
2252
|
expect(backfillCall[0]).toBe("test-conv");
|
|
2240
2253
|
expect(backfillCall[1]).toBe("mock-msg-id");
|
|
2241
2254
|
});
|
|
2255
|
+
|
|
2256
|
+
test("does not persist managed credential refresh failures as assistant text", async () => {
|
|
2257
|
+
mockConversationErrorClassification = {
|
|
2258
|
+
code: "MANAGED_KEY_INVALID",
|
|
2259
|
+
userMessage: "Couldn't refresh assistant credentials.",
|
|
2260
|
+
retryable: false,
|
|
2261
|
+
errorCategory: "managed_key_invalid",
|
|
2262
|
+
};
|
|
2263
|
+
const events: ServerMessage[] = [];
|
|
2264
|
+
|
|
2265
|
+
const ctx = makeCtx({
|
|
2266
|
+
loopProvider: {
|
|
2267
|
+
name: "mock-provider",
|
|
2268
|
+
async sendMessage() {
|
|
2269
|
+
throw new Error("API key has expired.");
|
|
2270
|
+
},
|
|
2271
|
+
} as unknown as Provider,
|
|
2272
|
+
});
|
|
2273
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
2274
|
+
|
|
2275
|
+
expect(
|
|
2276
|
+
events.filter((event) => event.type === "assistant_text_delta"),
|
|
2277
|
+
).toHaveLength(0);
|
|
2278
|
+
|
|
2279
|
+
const conversationError = events.find(
|
|
2280
|
+
(event) => event.type === "conversation_error",
|
|
2281
|
+
);
|
|
2282
|
+
expect(conversationError).toBeDefined();
|
|
2283
|
+
expect(conversationError).toMatchObject({
|
|
2284
|
+
code: "MANAGED_KEY_INVALID",
|
|
2285
|
+
userMessage: "Couldn't refresh assistant credentials.",
|
|
2286
|
+
errorCategory: "managed_key_invalid",
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
2290
|
+
expect(recordRequestLogMock).not.toHaveBeenCalled();
|
|
2291
|
+
expect(backfillMessageIdOnLogsMock).not.toHaveBeenCalled();
|
|
2292
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
2293
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
2294
|
+
string,
|
|
2295
|
+
];
|
|
2296
|
+
expect(deleteCall[0]).toBe("msg-reserve");
|
|
2297
|
+
});
|
|
2242
2298
|
});
|
|
2243
2299
|
|
|
2244
2300
|
describe("B3 pre-allocation: indexing + cleanup", () => {
|
|
@@ -2450,6 +2506,41 @@ describe("session-agent-loop", () => {
|
|
|
2450
2506
|
expect(lastSync?.[1]).toBe("mock-msg-id");
|
|
2451
2507
|
expect(lastSync?.[1]).not.toBe("msg-orphaned-reservation");
|
|
2452
2508
|
});
|
|
2509
|
+
|
|
2510
|
+
test("managed-key provider-error cleanup publishes message invalidation after deleting the reservation", async () => {
|
|
2511
|
+
reserveMessageMock.mockImplementationOnce(async () => ({
|
|
2512
|
+
id: "msg-managed-key-reservation",
|
|
2513
|
+
}));
|
|
2514
|
+
mockConversationErrorClassification = {
|
|
2515
|
+
code: "MANAGED_KEY_INVALID",
|
|
2516
|
+
userMessage: "Couldn't refresh assistant credentials.",
|
|
2517
|
+
retryable: false,
|
|
2518
|
+
errorCategory: "managed_key_invalid",
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
const ctx = makeCtx({
|
|
2522
|
+
loopProvider: {
|
|
2523
|
+
name: "mock-provider",
|
|
2524
|
+
async sendMessage() {
|
|
2525
|
+
throw new Error("API key has expired.");
|
|
2526
|
+
},
|
|
2527
|
+
} as unknown as Provider,
|
|
2528
|
+
});
|
|
2529
|
+
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
2530
|
+
|
|
2531
|
+
expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
|
|
2532
|
+
const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
|
|
2533
|
+
string,
|
|
2534
|
+
];
|
|
2535
|
+
expect(deleteCall[0]).toBe("msg-managed-key-reservation");
|
|
2536
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
2537
|
+
expect(syncMessageToDiskMock).not.toHaveBeenCalled();
|
|
2538
|
+
|
|
2539
|
+
const messagePublishes = (
|
|
2540
|
+
publishSyncInvalidationMock.mock.calls as unknown as Array<[string[]]>
|
|
2541
|
+
).filter((args) => args[0]?.includes("conversation:test-conv:messages"));
|
|
2542
|
+
expect(messagePublishes).toHaveLength(1);
|
|
2543
|
+
});
|
|
2453
2544
|
});
|
|
2454
2545
|
|
|
2455
2546
|
describe("partial persistence", () => {
|