@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c
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/docs/architecture/memory.md +9 -1
- package/docs/architecture/scheduling.md +13 -5
- package/docs/desktop-browser-cli.md +3 -2
- package/docs/guardian-request-flow.md +1 -1
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/openapi.yaml +152 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +98 -1
- package/src/__tests__/background-tool-registry.test.ts +39 -0
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
- package/src/__tests__/conversation-agent-loop.test.ts +529 -1
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-pairing.test.ts +69 -0
- package/src/__tests__/conversation-queue.test.ts +257 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
- package/src/__tests__/gemini-provider.test.ts +9 -0
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/introduction-card-resolver.test.ts +53 -8
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/non-member-access-request.test.ts +48 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
- package/src/__tests__/notification-platform-adapter.test.ts +97 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
- package/src/__tests__/scheduler-result-notification.test.ts +521 -7
- package/src/__tests__/scheduler-wake.test.ts +1 -0
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
- package/src/__tests__/subagent-manager-notify.test.ts +48 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
- package/src/agent/loop.ts +23 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/generation-handoff.ts +4 -0
- package/src/api/events/message-complete.ts +4 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +11 -0
- package/src/api/requests/guardian-actions.test.ts +45 -0
- package/src/api/requests/guardian-actions.ts +20 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/responses/home.ts +27 -0
- package/src/approvals/guardian-decision-primitive.ts +6 -4
- package/src/approvals/guardian-request-resolvers.ts +35 -30
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +12 -4
- package/src/calls/voice-escalation-judge.ts +2 -2
- package/src/calls/voice-leg-coordinator.ts +23 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +63 -20
- package/src/calls/voice-triage-escalate.ts +82 -27
- package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
- package/src/cli/commands/conversations.help.ts +7 -2
- package/src/cli/commands/conversations.ts +36 -30
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/bundled-marketplace.json +35 -0
- package/src/cli/lib/bundled-plugin-packages.json +21 -0
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/profile-order.test.ts +19 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-order.ts +7 -1
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
- package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
- package/src/daemon/activity-trail.test.ts +206 -0
- package/src/daemon/activity-trail.ts +190 -0
- package/src/daemon/auto-profile-router.ts +431 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +157 -32
- package/src/daemon/conversation-error.ts +19 -0
- package/src/daemon/conversation-lifecycle.ts +70 -7
- package/src/daemon/conversation-media-retry.ts +1 -2
- package/src/daemon/conversation-messaging.ts +31 -2
- package/src/daemon/conversation-process.ts +107 -16
- package/src/daemon/conversation-queue-manager.ts +7 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/conversation-turn-finalize.ts +14 -0
- package/src/daemon/conversation.ts +25 -6
- package/src/daemon/event-loop-watchdog.test.ts +201 -2
- package/src/daemon/event-loop-watchdog.ts +182 -6
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/wake-conversation-ops.ts +4 -2
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +25 -19
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/discord/withdraw.ts +4 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/messaging/providers/slack/withdraw.ts +5 -5
- package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
- package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
- package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
- package/src/monitoring/__tests__/process-usage.test.ts +243 -0
- package/src/monitoring/proc-wait-state.ts +366 -0
- package/src/monitoring/process-memory.ts +4 -3
- package/src/monitoring/process-usage.ts +215 -0
- package/src/monitoring/resource-sample-types.ts +8 -0
- package/src/monitoring/resource-sampler.ts +17 -2
- package/src/monitoring/stall-capture.ts +12 -0
- package/src/notifications/README.md +88 -9
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
- package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
- package/src/notifications/__tests__/broadcaster.test.ts +292 -1
- package/src/notifications/__tests__/completion-policy.test.ts +243 -0
- package/src/notifications/__tests__/decision-engine.test.ts +51 -4
- package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
- package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
- package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
- package/src/notifications/adapters/macos.ts +24 -12
- package/src/notifications/adapters/platform.ts +16 -5
- package/src/notifications/assistant-reply-producer.ts +96 -71
- package/src/notifications/background-result-producer.ts +369 -0
- package/src/notifications/broadcaster.ts +28 -8
- package/src/notifications/completion-policy.ts +115 -0
- package/src/notifications/completion-work.ts +46 -0
- package/src/notifications/conversation-pairing.ts +32 -13
- package/src/notifications/decision-engine.ts +18 -24
- package/src/notifications/emit-signal.ts +16 -1
- package/src/notifications/events-store.ts +4 -0
- package/src/notifications/has-pending-background-work.ts +91 -0
- package/src/notifications/home-feed-mirror.ts +44 -0
- package/src/notifications/home-feed-side-effect.ts +54 -28
- package/src/notifications/resolve-visible-in-source.ts +56 -2
- package/src/notifications/result-output.ts +154 -0
- package/src/notifications/schedule-result-producer.ts +6 -187
- package/src/notifications/types.ts +2 -0
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
- package/src/persistence/conversation-crud.ts +26 -0
- package/src/persistence/conversation-types.ts +15 -37
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/injector.ts +4 -4
- package/src/plugins/types.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +4 -0
- package/src/providers/gemini/client.ts +7 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/inference/profile-probe.ts +1 -0
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
- package/src/providers/openai/api-error-normalization.ts +4 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +7 -0
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/__tests__/agent-wake.test.ts +308 -0
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake-queue.ts +52 -0
- package/src/runtime/agent-wake.ts +111 -18
- package/src/runtime/channel-approval-types.ts +6 -38
- package/src/runtime/channel-approvals.ts +4 -5
- package/src/runtime/http-server.ts +4 -1
- package/src/runtime/introduction-policy.test.ts +17 -1
- package/src/runtime/introduction-policy.ts +25 -7
- package/src/runtime/migrations/vbundle-builder.ts +9 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/auto-profile-routes.ts +83 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -3
- package/src/runtime/routes/conversation-routes.ts +9 -0
- package/src/runtime/routes/guardian-action-routes.ts +8 -23
- package/src/runtime/routes/host-cu-routes.ts +3 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +5 -2
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/migration-routes.ts +8 -2
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/user-route-dispatcher.ts +1 -2
- package/src/runtime/web-presence.ts +3 -4
- package/src/schedule/schedule-store.ts +10 -0
- package/src/schedule/scheduler.ts +92 -24
- package/src/subagent/manager.ts +32 -12
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/tools/background-tool-registry.ts +30 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +7 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/load.ts +13 -17
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/terminal/shell-launch.test.ts +27 -1
- package/src/tools/terminal/shell.ts +4 -0
- package/src/tools/workspace-tools/loader.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/abort-reasons.ts +2 -0
- package/src/util/errors.ts +1 -0
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/provider-error-patterns.ts +11 -0
- package/src/util/single-flight.test.ts +6 -0
- package/src/util/single-flight.ts +2 -0
- package/src/util/worker-pipe.test.ts +56 -10
- package/src/util/worker-pipe.ts +9 -4
- package/src/workspace/git-service.ts +6 -1
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -927,6 +927,65 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
927
927
|
expect(messagesInvalidated).toEqual([]);
|
|
928
928
|
});
|
|
929
929
|
|
|
930
|
+
test("explicit background result keeps its persisted result without another preview row", async () => {
|
|
931
|
+
mockExistingConversations["conv-user-chat"] = {
|
|
932
|
+
id: "conv-user-chat",
|
|
933
|
+
source: "user",
|
|
934
|
+
title: "An ordinary chat",
|
|
935
|
+
};
|
|
936
|
+
const result = await pairDeliveryWithConversation(
|
|
937
|
+
makeSignal({
|
|
938
|
+
requiresConversation: undefined,
|
|
939
|
+
sourceContextId: "task-1",
|
|
940
|
+
sourceEventName: "activity.complete",
|
|
941
|
+
contextPayload: {
|
|
942
|
+
completion: {
|
|
943
|
+
workId: "task-1",
|
|
944
|
+
conversationId: "conv-user-chat",
|
|
945
|
+
recipientPrincipalId: "principal-1",
|
|
946
|
+
owner: "parent_continuation",
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
}),
|
|
950
|
+
"vellum",
|
|
951
|
+
makeCopy({ body: "The report is ready." }),
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
expect(result.conversationId).toBe("conv-user-chat");
|
|
955
|
+
expect(result.messageId).toBeNull();
|
|
956
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
957
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test("a missing declared completion conversation does not fall back to unrelated source content", async () => {
|
|
961
|
+
mockExistingConversations["conv-source"] = {
|
|
962
|
+
id: "conv-source",
|
|
963
|
+
source: "user",
|
|
964
|
+
title: "Another chat",
|
|
965
|
+
};
|
|
966
|
+
const result = await pairDeliveryWithConversation(
|
|
967
|
+
makeSignal({
|
|
968
|
+
requiresConversation: undefined,
|
|
969
|
+
sourceContextId: "conv-source",
|
|
970
|
+
sourceEventName: "activity.complete",
|
|
971
|
+
contextPayload: {
|
|
972
|
+
completion: {
|
|
973
|
+
workId: "task-1",
|
|
974
|
+
conversationId: "conv-missing",
|
|
975
|
+
recipientPrincipalId: "principal-1",
|
|
976
|
+
owner: "parent_continuation",
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
}),
|
|
980
|
+
"vellum",
|
|
981
|
+
makeCopy({ body: "The report is ready." }),
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
expect(result.conversationId).toBeNull();
|
|
985
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
986
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
987
|
+
});
|
|
988
|
+
|
|
930
989
|
test("passive vellum signal appends the body to the producing conversation", async () => {
|
|
931
990
|
mockExistingConversations["conv-producer"] = {
|
|
932
991
|
id: "conv-producer",
|
|
@@ -996,6 +1055,12 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
996
1055
|
expect(createConversationMock).not.toHaveBeenCalled();
|
|
997
1056
|
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
998
1057
|
expect(messagesInvalidated).toContain("conv-foreground");
|
|
1058
|
+
// The transcript is this notification's only home, so a chat marked Done
|
|
1059
|
+
// comes back for it.
|
|
1060
|
+
const options = addMessageMock.mock.calls[0]![3] as {
|
|
1061
|
+
skipResurface?: boolean;
|
|
1062
|
+
};
|
|
1063
|
+
expect(options.skipResurface).toBe(false);
|
|
999
1064
|
});
|
|
1000
1065
|
|
|
1001
1066
|
test("appended notification body skips indexing", async () => {
|
|
@@ -1017,8 +1082,12 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
1017
1082
|
|
|
1018
1083
|
const options = addMessageMock.mock.calls[0]![3] as {
|
|
1019
1084
|
skipIndexing?: boolean;
|
|
1085
|
+
skipResurface?: boolean;
|
|
1020
1086
|
};
|
|
1021
1087
|
expect(options.skipIndexing).toBe(true);
|
|
1088
|
+
// The bell carries this one (async-background), so the append is
|
|
1089
|
+
// bookkeeping and never resurfaces a chat the user marked Done.
|
|
1090
|
+
expect(options.skipResurface).toBe(true);
|
|
1022
1091
|
});
|
|
1023
1092
|
|
|
1024
1093
|
test("passive vellum signal appends regardless of the producing conversation's source", async () => {
|
|
@@ -46,6 +46,7 @@ const capturedPersistedSeqs: Array<{ id: string; seq: number }> = [];
|
|
|
46
46
|
* tail message while its siblings succeed).
|
|
47
47
|
*/
|
|
48
48
|
const addMessageShouldThrowForContent = new Set<string>();
|
|
49
|
+
let beforeMessageInsert: ((requestId?: string) => Promise<void>) | undefined;
|
|
49
50
|
|
|
50
51
|
mock.module("../prompts/system-prompt.js", () => ({
|
|
51
52
|
buildSystemPrompt: () => "system prompt",
|
|
@@ -101,12 +102,20 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
101
102
|
totalEstimatedCost: 0,
|
|
102
103
|
}),
|
|
103
104
|
createConversation: () => ({ id: "conv-1" }),
|
|
104
|
-
addMessage: (
|
|
105
|
+
addMessage: async (
|
|
105
106
|
_convId: string,
|
|
106
107
|
role: string,
|
|
107
108
|
content: string,
|
|
108
|
-
options?: {
|
|
109
|
+
options?: {
|
|
110
|
+
id?: string;
|
|
111
|
+
metadata?: Record<string, unknown>;
|
|
112
|
+
insertPrecondition?: () => boolean;
|
|
113
|
+
},
|
|
109
114
|
) => {
|
|
115
|
+
await beforeMessageInsert?.(options?.id);
|
|
116
|
+
if (options?.insertPrecondition && !options.insertPrecondition()) {
|
|
117
|
+
throw new Error("Message insert cancelled");
|
|
118
|
+
}
|
|
110
119
|
// Simulate a persist failure for tests that need to exercise the
|
|
111
120
|
// tail-persist-failed path in drainBatch. Triggered by matching any
|
|
112
121
|
// registered substring against the serialized content payload.
|
|
@@ -115,7 +124,7 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
115
124
|
throw new Error(`Simulated addMessage failure for content: ${needle}`);
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
|
-
const id = `msg-${Date.now()}-${capturedAddMessages.length}`;
|
|
127
|
+
const id = options?.id ?? `msg-${Date.now()}-${capturedAddMessages.length}`;
|
|
119
128
|
capturedAddMessages.push({
|
|
120
129
|
id,
|
|
121
130
|
role,
|
|
@@ -126,7 +135,8 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
126
135
|
},
|
|
127
136
|
updateConversationUsage: () => {},
|
|
128
137
|
updateConversationTitle: () => {},
|
|
129
|
-
getMessageById: () =>
|
|
138
|
+
getMessageById: (id: string) =>
|
|
139
|
+
capturedAddMessages.find((message) => message.id === id) ?? null,
|
|
130
140
|
getLastUserTimestampBefore: () => 0,
|
|
131
141
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
132
142
|
updateMessageContent: mock(() => {}),
|
|
@@ -481,10 +491,12 @@ async function resolveRun(index: number) {
|
|
|
481
491
|
}
|
|
482
492
|
|
|
483
493
|
beforeEach(() => {
|
|
494
|
+
capturedAddMessages.length = 0;
|
|
484
495
|
turnCommitCalls.length = 0;
|
|
485
496
|
turnCommitHangForever = false;
|
|
486
497
|
linkAttachmentShouldThrow = false;
|
|
487
498
|
addMessageShouldThrowForContent.clear();
|
|
499
|
+
beforeMessageInsert = undefined;
|
|
488
500
|
});
|
|
489
501
|
|
|
490
502
|
afterAll(() => {
|
|
@@ -1049,6 +1061,247 @@ describe("Conversation message queue", () => {
|
|
|
1049
1061
|
expect(pendingRuns.length).toBe(3);
|
|
1050
1062
|
});
|
|
1051
1063
|
|
|
1064
|
+
for (const cronRunId of ["run-scheduled", null]) {
|
|
1065
|
+
for (const batchSize of [1, 2]) {
|
|
1066
|
+
for (const cancel of [
|
|
1067
|
+
false,
|
|
1068
|
+
"schedule",
|
|
1069
|
+
"stop",
|
|
1070
|
+
"signal",
|
|
1071
|
+
"dispose",
|
|
1072
|
+
] as const) {
|
|
1073
|
+
if (cronRunId === null && cancel === "schedule") {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
for (const claimed of [false, true]) {
|
|
1077
|
+
test(`queued dispatch stays tracked during persistence (owner=${cronRunId}, batch=${batchSize}, cancel=${cancel}, claimed=${claimed})`, async () => {
|
|
1078
|
+
const conversation = makeConversation();
|
|
1079
|
+
await conversation.loadFromDb();
|
|
1080
|
+
const persistedBefore = capturedAddMessages.length;
|
|
1081
|
+
const entered = Promise.withResolvers<void>();
|
|
1082
|
+
const release = Promise.withResolvers<void>();
|
|
1083
|
+
const acquire =
|
|
1084
|
+
conversation.acquireProcessingFenced.bind(conversation);
|
|
1085
|
+
conversation.acquireProcessingFenced = async () => {
|
|
1086
|
+
const owner = claimed ? await acquire() : undefined;
|
|
1087
|
+
entered.resolve();
|
|
1088
|
+
await release.promise;
|
|
1089
|
+
return owner === undefined ? acquire() : owner;
|
|
1090
|
+
};
|
|
1091
|
+
const events: AssistantEvent[] = [];
|
|
1092
|
+
conversation.setProcessing(true);
|
|
1093
|
+
for (let i = 0; i < batchSize; i++) {
|
|
1094
|
+
conversation.enqueueMessage({
|
|
1095
|
+
content: `Queued continuation ${i}`,
|
|
1096
|
+
requestId: `queued-${i}`,
|
|
1097
|
+
clientMessageId: `client-queued-${i}`,
|
|
1098
|
+
cronRunId,
|
|
1099
|
+
onEvent: (event) => events.push(event),
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
conversation.setProcessing(false);
|
|
1103
|
+
const drain = conversation.drainQueue();
|
|
1104
|
+
try {
|
|
1105
|
+
await entered.promise;
|
|
1106
|
+
expect(conversation.getQueueDepth()).toBe(0);
|
|
1107
|
+
expect(conversation.isProcessing()).toBe(claimed);
|
|
1108
|
+
expect(
|
|
1109
|
+
conversation.pendingQueuedDispatches.get(cronRunId)?.size,
|
|
1110
|
+
).toBe(1);
|
|
1111
|
+
expect(conversation.hasInFlightWork()).toBe(true);
|
|
1112
|
+
if (cancel === "schedule") {
|
|
1113
|
+
conversation.abortScheduledRun("run-scheduled");
|
|
1114
|
+
} else if (cancel === "stop" || cancel === "signal") {
|
|
1115
|
+
conversation.abort(
|
|
1116
|
+
createAbortReason(
|
|
1117
|
+
cancel === "stop" ? "user_cancel" : "signal_cancel",
|
|
1118
|
+
"test",
|
|
1119
|
+
),
|
|
1120
|
+
);
|
|
1121
|
+
} else if (cancel === "dispose") {
|
|
1122
|
+
conversation.dispose();
|
|
1123
|
+
}
|
|
1124
|
+
} finally {
|
|
1125
|
+
release.resolve();
|
|
1126
|
+
await drain;
|
|
1127
|
+
}
|
|
1128
|
+
expect(conversation.pendingQueuedDispatches.size).toBe(0);
|
|
1129
|
+
const preserved =
|
|
1130
|
+
cronRunId === null && (cancel === "stop" || cancel === "signal");
|
|
1131
|
+
if (cancel && !preserved) {
|
|
1132
|
+
expect(pendingRuns).toHaveLength(0);
|
|
1133
|
+
expect(capturedAddMessages).toHaveLength(persistedBefore);
|
|
1134
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
1135
|
+
expect(conversation.currentTurnCronRunId).toBeUndefined();
|
|
1136
|
+
expect(conversation.getQueueDepth()).toBe(0);
|
|
1137
|
+
expect(
|
|
1138
|
+
events.filter((event) => event.type === "generation_cancelled"),
|
|
1139
|
+
).toHaveLength(batchSize);
|
|
1140
|
+
for (let i = 0; i < batchSize; i++) {
|
|
1141
|
+
const queueEvents = events.filter(
|
|
1142
|
+
(event) =>
|
|
1143
|
+
"requestId" in event && event.requestId === `queued-${i}`,
|
|
1144
|
+
);
|
|
1145
|
+
expect(queueEvents.map((event) => event.type)).toEqual(
|
|
1146
|
+
i === 0
|
|
1147
|
+
? [
|
|
1148
|
+
"message_queued",
|
|
1149
|
+
"message_dequeued",
|
|
1150
|
+
"message_requeued",
|
|
1151
|
+
"message_queued_deleted",
|
|
1152
|
+
]
|
|
1153
|
+
: ["message_queued", "message_queued_deleted"],
|
|
1154
|
+
);
|
|
1155
|
+
expect(queueEvents.at(-1)).toMatchObject({
|
|
1156
|
+
clientMessageId: `client-queued-${i}`,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
await waitForPendingRun(1);
|
|
1161
|
+
expect(conversation.currentTurnCronRunId).toBe(cronRunId);
|
|
1162
|
+
expect(conversation.currentTurnWorkOrigins).toHaveLength(
|
|
1163
|
+
batchSize,
|
|
1164
|
+
);
|
|
1165
|
+
if (preserved) {
|
|
1166
|
+
expect(capturedAddMessages.length).toBe(
|
|
1167
|
+
persistedBefore + batchSize,
|
|
1168
|
+
);
|
|
1169
|
+
expect(
|
|
1170
|
+
events.filter((event) => event.type === "user_message_echo"),
|
|
1171
|
+
).toHaveLength(batchSize);
|
|
1172
|
+
expect(
|
|
1173
|
+
events.some(
|
|
1174
|
+
(event) => event.type === "message_queued_deleted",
|
|
1175
|
+
),
|
|
1176
|
+
).toBe(false);
|
|
1177
|
+
}
|
|
1178
|
+
await resolveRun(0);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
for (const cancel of ["schedule", "dispose"] as const) {
|
|
1187
|
+
test(`${cancel} cancels an unpersisted batch tail without deleting its persisted head`, async () => {
|
|
1188
|
+
const conversation = makeConversation();
|
|
1189
|
+
await conversation.loadFromDb();
|
|
1190
|
+
const entered = Promise.withResolvers<void>();
|
|
1191
|
+
const release = Promise.withResolvers<void>();
|
|
1192
|
+
beforeMessageInsert = async (requestId) => {
|
|
1193
|
+
if (requestId === "queued-tail") {
|
|
1194
|
+
entered.resolve();
|
|
1195
|
+
await release.promise;
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const events: AssistantEvent[] = [];
|
|
1199
|
+
conversation.setProcessing(true);
|
|
1200
|
+
for (const requestId of ["queued-head", "queued-tail"]) {
|
|
1201
|
+
conversation.enqueueMessage({
|
|
1202
|
+
content: requestId,
|
|
1203
|
+
requestId,
|
|
1204
|
+
clientMessageId: `client-${requestId}`,
|
|
1205
|
+
cronRunId: "run-scheduled",
|
|
1206
|
+
onEvent: (event) => events.push(event),
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
conversation.setProcessing(false);
|
|
1210
|
+
const drain = conversation.drainQueue();
|
|
1211
|
+
try {
|
|
1212
|
+
await entered.promise;
|
|
1213
|
+
expect(
|
|
1214
|
+
events.some(
|
|
1215
|
+
(event) =>
|
|
1216
|
+
event.type === "user_message_echo" &&
|
|
1217
|
+
event.requestId === "queued-head",
|
|
1218
|
+
),
|
|
1219
|
+
).toBe(true);
|
|
1220
|
+
if (cancel === "schedule") {
|
|
1221
|
+
conversation.abortScheduledRun("run-scheduled");
|
|
1222
|
+
conversation.abortScheduledRun("run-scheduled");
|
|
1223
|
+
} else {
|
|
1224
|
+
conversation.dispose();
|
|
1225
|
+
}
|
|
1226
|
+
expect(
|
|
1227
|
+
events.filter((event) => event.type === "generation_cancelled"),
|
|
1228
|
+
).toHaveLength(2);
|
|
1229
|
+
} finally {
|
|
1230
|
+
release.resolve();
|
|
1231
|
+
await drain;
|
|
1232
|
+
}
|
|
1233
|
+
expect(
|
|
1234
|
+
events
|
|
1235
|
+
.filter((event) => event.type === "message_queued_deleted")
|
|
1236
|
+
.map((event) => event.requestId),
|
|
1237
|
+
).toEqual(["queued-tail"]);
|
|
1238
|
+
expect(capturedAddMessages.map((message) => message.id)).toEqual([
|
|
1239
|
+
"queued-head",
|
|
1240
|
+
]);
|
|
1241
|
+
expect(conversation.getQueueDepth()).toBe(0);
|
|
1242
|
+
expect(conversation.pendingQueuedDispatches.size).toBe(0);
|
|
1243
|
+
if (pendingRuns.length > 0) {
|
|
1244
|
+
await resolveRun(0);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
for (const cancel of ["user_cancel", "signal_cancel"] as const) {
|
|
1250
|
+
for (const batchSize of [1, 2]) {
|
|
1251
|
+
test(`${cancel} preserves an accepted queued prompt after persistence (batch=${batchSize})`, async () => {
|
|
1252
|
+
const conversation = makeConversation();
|
|
1253
|
+
await conversation.loadFromDb();
|
|
1254
|
+
const persistedBefore = capturedAddMessages.length;
|
|
1255
|
+
const entered = Promise.withResolvers<void>();
|
|
1256
|
+
const release = Promise.withResolvers<void>();
|
|
1257
|
+
const persist = conversation.persistUserMessage.bind(conversation);
|
|
1258
|
+
let paused = false;
|
|
1259
|
+
conversation.persistUserMessage = async (options) => {
|
|
1260
|
+
const result = await persist(options);
|
|
1261
|
+
if (!paused) {
|
|
1262
|
+
paused = true;
|
|
1263
|
+
entered.resolve();
|
|
1264
|
+
await release.promise;
|
|
1265
|
+
}
|
|
1266
|
+
return result;
|
|
1267
|
+
};
|
|
1268
|
+
const events: AssistantEvent[] = [];
|
|
1269
|
+
conversation.setProcessing(true);
|
|
1270
|
+
for (let i = 0; i < batchSize; i++) {
|
|
1271
|
+
conversation.enqueueMessage({
|
|
1272
|
+
content: `User prompt ${i}`,
|
|
1273
|
+
requestId: `request-${i}`,
|
|
1274
|
+
clientMessageId: `client-${i}`,
|
|
1275
|
+
onEvent: (event) => events.push(event),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
conversation.setProcessing(false);
|
|
1279
|
+
const drain = conversation.drainQueue();
|
|
1280
|
+
try {
|
|
1281
|
+
await entered.promise;
|
|
1282
|
+
expect(capturedAddMessages.length).toBe(persistedBefore + 1);
|
|
1283
|
+
conversation.abort(createAbortReason(cancel, "test"));
|
|
1284
|
+
expect(conversation.abortController?.signal.aborted).toBe(false);
|
|
1285
|
+
} finally {
|
|
1286
|
+
release.resolve();
|
|
1287
|
+
await drain;
|
|
1288
|
+
}
|
|
1289
|
+
await waitForPendingRun(1);
|
|
1290
|
+
expect(
|
|
1291
|
+
events.filter((event) => event.type === "user_message_echo"),
|
|
1292
|
+
).toHaveLength(batchSize);
|
|
1293
|
+
expect(
|
|
1294
|
+
events.some((event) => event.type === "message_queued_deleted"),
|
|
1295
|
+
).toBe(false);
|
|
1296
|
+
expect(capturedAddMessages.length).toBe(persistedBefore + batchSize);
|
|
1297
|
+
expect(conversation.pendingQueuedDispatches.size).toBe(0);
|
|
1298
|
+
conversation.abort(createAbortReason(cancel, "test:active-turn"));
|
|
1299
|
+
expect(conversation.abortController?.signal.aborted).toBe(true);
|
|
1300
|
+
await resolveRun(0);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1052
1305
|
test("[experimental] batched siblings run under their firing's cron run id", async () => {
|
|
1053
1306
|
// A batched drain runs after the enqueuing turn has ended, so the firing's
|
|
1054
1307
|
// attribution has to travel on the queued messages. Without it the batch's
|
|
@@ -4088,6 +4088,33 @@ describe("Slack channel chronological rendering — multi-thread", () => {
|
|
|
4088
4088
|
};
|
|
4089
4089
|
}
|
|
4090
4090
|
|
|
4091
|
+
test("fresh memory policy reaches injectors on initial assembly and re-entry", async () => {
|
|
4092
|
+
const seen = { ctx: null as TurnContext | null, commits: 0 };
|
|
4093
|
+
registerPluginInjectors(V3_PROBE_PLUGIN, [probeMemoryV3Injector(seen)]);
|
|
4094
|
+
try {
|
|
4095
|
+
setConversation(
|
|
4096
|
+
FALLBACK_CONVERSATION_ID,
|
|
4097
|
+
asConversation({
|
|
4098
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
4099
|
+
currentCallSite: "callAgent",
|
|
4100
|
+
currentTurnSkipMemoryRetrieval: true,
|
|
4101
|
+
}),
|
|
4102
|
+
);
|
|
4103
|
+
for (const isReInjection of [false, true]) {
|
|
4104
|
+
await applyRuntimeInjections(
|
|
4105
|
+
[{ role: "user", content: [{ type: "text", text: "circle that" }] }],
|
|
4106
|
+
{
|
|
4107
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
4108
|
+
reinjection: isReInjection,
|
|
4109
|
+
},
|
|
4110
|
+
);
|
|
4111
|
+
expect(seen.ctx?.skipMemoryRetrieval).toBe(true);
|
|
4112
|
+
}
|
|
4113
|
+
} finally {
|
|
4114
|
+
unregisterPluginInjectors(V3_PROBE_PLUGIN);
|
|
4115
|
+
}
|
|
4116
|
+
});
|
|
4117
|
+
|
|
4091
4118
|
test("slack replacement is stated on the turn context and attaches the memory-v3 block in memory only: uncaptured, uncommitted", async () => {
|
|
4092
4119
|
const seen = { ctx: null as TurnContext | null, commits: 0 };
|
|
4093
4120
|
registerPluginInjectors(V3_PROBE_PLUGIN, [probeMemoryV3Injector(seen)]);
|
|
@@ -359,6 +359,68 @@ describe("access-request vellum floor", () => {
|
|
|
359
359
|
);
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
test("an all_channels access request the decision engine suppressed still fans out", async () => {
|
|
363
|
+
evaluateSignalMock.mockResolvedValue({
|
|
364
|
+
shouldNotify: false,
|
|
365
|
+
selectedChannels: [],
|
|
366
|
+
reasoningSummary: "LLM suppressed",
|
|
367
|
+
renderedCopy: {},
|
|
368
|
+
dedupeKey: "dedupe-ar-email",
|
|
369
|
+
confidence: 0.9,
|
|
370
|
+
fallbackUsed: false,
|
|
371
|
+
persistedDecisionId: "dec-ar-email",
|
|
372
|
+
});
|
|
373
|
+
// The real enforcement leaves a suppressed decision alone and replaces
|
|
374
|
+
// the selection with every connected channel otherwise.
|
|
375
|
+
enforceRoutingIntentMock.mockImplementation(
|
|
376
|
+
(
|
|
377
|
+
decision: { shouldNotify: boolean; selectedChannels: string[] },
|
|
378
|
+
intent: string | undefined,
|
|
379
|
+
connected: string[],
|
|
380
|
+
) =>
|
|
381
|
+
intent === "all_channels" && decision.shouldNotify
|
|
382
|
+
? { ...decision, selectedChannels: [...connected] }
|
|
383
|
+
: decision,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
await emitNotificationSignal({
|
|
387
|
+
sourceEventName: "ingress.access_request",
|
|
388
|
+
sourceChannel: "email",
|
|
389
|
+
sourceContextId: "access-req-email-stranger",
|
|
390
|
+
requiresConversation: true,
|
|
391
|
+
routingIntent: "all_channels",
|
|
392
|
+
attentionHints: {
|
|
393
|
+
requiresAction: true,
|
|
394
|
+
urgency: "high",
|
|
395
|
+
isAsyncBackground: false,
|
|
396
|
+
visibleInSourceNow: false,
|
|
397
|
+
},
|
|
398
|
+
contextPayload: {
|
|
399
|
+
requestId: "req-email",
|
|
400
|
+
requestCode: "EM12CD",
|
|
401
|
+
sourceChannel: "email",
|
|
402
|
+
conversationExternalId: "<thread-1@example.com>",
|
|
403
|
+
actorExternalId: "stranger@example.com",
|
|
404
|
+
actorDisplayName: null,
|
|
405
|
+
actorUsername: null,
|
|
406
|
+
senderIdentifier: "stranger@example.com",
|
|
407
|
+
guardianBindingChannel: null,
|
|
408
|
+
guardianResolutionSource: "vellum-anchor",
|
|
409
|
+
previousMemberStatus: null,
|
|
410
|
+
messagePreview: null,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const enforced = enforceRoutingIntentMock.mock.calls[0][0] as {
|
|
415
|
+
shouldNotify: boolean;
|
|
416
|
+
};
|
|
417
|
+
expect(enforced.shouldNotify).toBe(true);
|
|
418
|
+
const dispatched = dispatchDecisionMock.mock.calls[0][1] as {
|
|
419
|
+
selectedChannels: string[];
|
|
420
|
+
};
|
|
421
|
+
expect(dispatched.selectedChannels).toContain("vellum");
|
|
422
|
+
});
|
|
423
|
+
|
|
362
424
|
test("re-adds vellum when single_channel routing enforcement strips it", async () => {
|
|
363
425
|
evaluateSignalMock.mockResolvedValue({
|
|
364
426
|
shouldNotify: true,
|
|
@@ -49,6 +49,7 @@ function register(
|
|
|
49
49
|
processing: boolean;
|
|
50
50
|
queued: boolean;
|
|
51
51
|
modeSessionWork?: boolean;
|
|
52
|
+
scheduledDispatch?: boolean;
|
|
52
53
|
stale?: boolean;
|
|
53
54
|
},
|
|
54
55
|
): FakeConversation {
|
|
@@ -57,6 +58,14 @@ function register(
|
|
|
57
58
|
markedStale: false,
|
|
58
59
|
conversationId: id,
|
|
59
60
|
liveVoiceResidencyLeases: 0,
|
|
61
|
+
pendingQueuedDispatches: state.scheduledDispatch
|
|
62
|
+
? new Map([
|
|
63
|
+
[
|
|
64
|
+
"run-scheduled",
|
|
65
|
+
new Set([{ controller: new AbortController(), messages: [] }]),
|
|
66
|
+
],
|
|
67
|
+
])
|
|
68
|
+
: new Map(),
|
|
60
69
|
isProcessing: () => state.processing,
|
|
61
70
|
hasQueuedMessages: () => state.queued,
|
|
62
71
|
modeSessions: {
|
|
@@ -111,6 +120,18 @@ describe("evictConversationsForReload", () => {
|
|
|
111
120
|
expect(abortedParents).toEqual([]);
|
|
112
121
|
});
|
|
113
122
|
|
|
123
|
+
test("keeps a scheduled dispatch whose processing claim is pending", () => {
|
|
124
|
+
const pending = register("reload-scheduled", {
|
|
125
|
+
processing: false,
|
|
126
|
+
queued: false,
|
|
127
|
+
scheduledDispatch: true,
|
|
128
|
+
});
|
|
129
|
+
evictConversationsForReload();
|
|
130
|
+
expect(pending.disposed).toBe(false);
|
|
131
|
+
expect(pending.markedStale).toBe(true);
|
|
132
|
+
expect(abortedParents).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
114
135
|
test("marks a mid-turn conversation stale instead of disposing it", () => {
|
|
115
136
|
const busy = register("reload-busy", { processing: true, queued: false });
|
|
116
137
|
|
|
@@ -1748,6 +1748,15 @@ describe("GeminiProvider", () => {
|
|
|
1748
1748
|
}
|
|
1749
1749
|
});
|
|
1750
1750
|
|
|
1751
|
+
test("maps a free-tier daily-limit 402 body code to free_tier_daily_limit_reached", async () => {
|
|
1752
|
+
expect(
|
|
1753
|
+
await reasonForApiError(
|
|
1754
|
+
402,
|
|
1755
|
+
'{"code":"free_tier_daily_limit_reached","detail":"free usage"}',
|
|
1756
|
+
),
|
|
1757
|
+
).toBe("free_tier_daily_limit_reached");
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1751
1760
|
test("maps a daily-limit 402 body code to daily_limit_reached", async () => {
|
|
1752
1761
|
expect(
|
|
1753
1762
|
await reasonForApiError(
|
|
@@ -1186,6 +1186,85 @@ describe("HostCuProxy", () => {
|
|
|
1186
1186
|
|
|
1187
1187
|
expect(result.content).toContain("2560x1440 px");
|
|
1188
1188
|
expect(result.content).toContain("1280x720 pt");
|
|
1189
|
+
expect(result.content).toContain("x = round(image_x * 0.500000)");
|
|
1190
|
+
expect(result.content).toContain("y = round(image_y * 0.500000)");
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
test("derives separate screen-point scales for a downscaled screenshot", () => {
|
|
1194
|
+
setup();
|
|
1195
|
+
|
|
1196
|
+
const result = proxy.formatObservation({
|
|
1197
|
+
screenshot: "base64data",
|
|
1198
|
+
screenshotWidthPx: 835,
|
|
1199
|
+
screenshotHeightPx: 540,
|
|
1200
|
+
screenWidthPt: 1728,
|
|
1201
|
+
screenHeightPt: 1117,
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
const xScale = Number(result.content.match(/image_x \* ([\d.]+)/)?.[1]);
|
|
1205
|
+
const yScale = Number(result.content.match(/image_y \* ([\d.]+)/)?.[1]);
|
|
1206
|
+
expect(Math.round(675 * xScale)).toBe(1397);
|
|
1207
|
+
expect(Math.round(330 * yScale)).toBe(683);
|
|
1208
|
+
expect(result.content).toContain("screen points, not screenshot pixels");
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test.each([
|
|
1212
|
+
{ capture_window_id: 12 },
|
|
1213
|
+
{ captureWindowId: 12 },
|
|
1214
|
+
{ captureDisplayId: 2 },
|
|
1215
|
+
])(
|
|
1216
|
+
"does not scale a scoped screenshot by desktop dimensions: %j",
|
|
1217
|
+
(input) => {
|
|
1218
|
+
setup();
|
|
1219
|
+
|
|
1220
|
+
const result = proxy.formatObservation(
|
|
1221
|
+
{
|
|
1222
|
+
screenshot: "base64data",
|
|
1223
|
+
screenshotWidthPx: 800,
|
|
1224
|
+
screenshotHeightPx: 600,
|
|
1225
|
+
screenWidthPt: 1600,
|
|
1226
|
+
screenHeightPt: 1200,
|
|
1227
|
+
},
|
|
1228
|
+
undefined,
|
|
1229
|
+
false,
|
|
1230
|
+
{ toolName: "computer_use_observe", input },
|
|
1231
|
+
);
|
|
1232
|
+
|
|
1233
|
+
expect(result.content).toContain("screenshot is capture-relative");
|
|
1234
|
+
expect(result.content).not.toContain("round(image_");
|
|
1235
|
+
},
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
test.each([undefined, 0, -1, Number.NaN, Number.POSITIVE_INFINITY])(
|
|
1239
|
+
"does not invent a mapping for an unavailable or invalid dimension: %s",
|
|
1240
|
+
(screenWidthPt) => {
|
|
1241
|
+
setup();
|
|
1242
|
+
|
|
1243
|
+
const result = proxy.formatObservation({
|
|
1244
|
+
screenshot: "base64data",
|
|
1245
|
+
screenshotWidthPx: 800,
|
|
1246
|
+
screenshotHeightPx: 600,
|
|
1247
|
+
screenWidthPt,
|
|
1248
|
+
screenHeightPt: 1200,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
expect(result.content).toContain("coordinate mapping is unavailable");
|
|
1252
|
+
expect(result.content).not.toContain("round(image_");
|
|
1253
|
+
},
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
test("omits coordinate guidance when no screenshot accompanies dimensions", () => {
|
|
1257
|
+
setup();
|
|
1258
|
+
|
|
1259
|
+
const result = proxy.formatObservation({
|
|
1260
|
+
axTree: "Button [1]",
|
|
1261
|
+
screenshotWidthPx: 800,
|
|
1262
|
+
screenshotHeightPx: 600,
|
|
1263
|
+
screenWidthPt: 1600,
|
|
1264
|
+
screenHeightPt: 1200,
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
expect(result.content).not.toContain("coordinate");
|
|
1189
1268
|
});
|
|
1190
1269
|
|
|
1191
1270
|
test("escapes </ax-tree> in AX tree content", () => {
|